Skip to content

test: cover glossary CSV typed relations across unit, IT, Playwright#27740

Open
harshach wants to merge 3 commits intomainfrom
harshach/glossary-relations-io
Open

test: cover glossary CSV typed relations across unit, IT, Playwright#27740
harshach wants to merge 3 commits intomainfrom
harshach/glossary-relations-io

Conversation

@harshach
Copy link
Copy Markdown
Collaborator

@harshach harshach commented Apr 26, 2026

Describe your changes:

PR #25886 added typed glossary term relations and the CSV import/export plumbing to carry the relationType:fqn format. This PR adds test coverage only to lock that round-trip down end-to-end so the typed relations cannot silently regress to bare-FQN behavior.

  • Unit (CsvUtilTest): 5 new tests for CsvUtil.addTermRelations() — null/empty input, default relatedTo prefix omission, null relationType handling, prefixed non-default types, and mixed-type alphabetic sort.
  • Integration (GlossaryCsvRelationTypesIT): 4 strong tests — mixed-format import verified per-term via API, asymmetric relation export visible from both sides (broader/narrower), full export→re-import round-trip with three relation types preserved, and custom relation type round-trip via GlossaryTermRelationSettings (creates + cleans up the custom type).
  • Playwright (GlossaryImportExport.spec.ts): 2 new specs — UI bulk import of CSV with mixed typed relations verifying status + API-fetched relation types + export CSV content; negative spec asserting an unknown relation type surfaces the "Invalid relation type" error in the import grid.

No production code changes. Verified locally: mvn test -pl openmetadata-service -Dtest=CsvUtilTest passes 10/10 and mvn spotless:check is clean across both Java modules; eslint clean on the Playwright file.

Type of change:

  • Improvement

Checklist:

  • I have read the CONTRIBUTING document.
  • I have added tests around the new logic.

🤖 Generated with Claude Code


Summary by Gitar

  • Test stability:
    • Added @Isolated annotation to GlossaryOntologyExportIT and GlossaryTermRelationIT to prevent state contamination and flakes during concurrent test execution.

This will update automatically on new commits.

…d Playwright

Add round-trip and validation coverage for typed glossary term relations
(synonym, broader, narrower, custom types from settings) in CSV
import/export. Unit tests in CsvUtilTest exercise addTermRelations
serialization edge cases. Integration tests in GlossaryCsvRelationTypesIT
add a true export-reimport round-trip, mixed-format per-term API
verification, asymmetric relation visibility from both sides, and a
custom-relation-type round-trip via GlossaryTermRelationSettings.
Playwright specs in GlossaryImportExport.spec.ts drive the UI bulk
import/export flow with typed relations and assert the validation
surface rejects unknown relation types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 26, 2026 16:35
@harshach harshach requested a review from a team as a code owner April 26, 2026 16:35
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Apr 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds test coverage to ensure glossary CSV import/export round-trips typed glossary term relations (relationType:fqn) across unit, integration, and UI E2E layers, preventing regressions back to bare-FQN behavior.

Changes:

  • Added Playwright E2E specs to validate typed relation import (including export verification) and invalid relation type errors.
  • Added CsvUtil.addTermRelations() unit tests covering null/empty inputs, default handling, prefix emission, and sorting/mixed types.
  • Added integration tests validating mixed-format import, asymmetric inverse export behavior, full export→reimport preservation, and custom relation type round-trip via settings.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts Adds E2E coverage for typed relation import/export and invalid relation type error surfacing
openmetadata-service/src/test/java/org/openmetadata/csv/CsvUtilTest.java Adds unit tests for typed relation CSV formatting behavior in CsvUtil.addTermRelations()
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryCsvRelationTypesIT.java Adds IT coverage for typed relation import/export behavior and custom relation type round-trip

Comment on lines +828 to +833
const stream = await download.createReadStream();
const chunks: Buffer[] = [];

for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

download.createReadStream() can return null (e.g., depending on browser/context), but the test unconditionally iterates for await (const chunk of stream), which will throw if the stream is null. Handle the null case (as done earlier in this spec) or assert the stream is non-null before reading.

Copilot uses AI. Check for mistakes.

test('Glossary CSV import preserves typed relations', async ({ page }) => {
const { apiContext, afterAction } = await getApiContext(page);
const relGlossary = new Glossary('TypedRelations');
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glossary names TypedRelations / TypedRelationsInvalid are hard-coded. With Playwright configured as fullyParallel: true, this can cause intermittent failures when multiple workers (or reruns) attempt to create the same glossary name concurrently. Prefer including a uuid() suffix/prefix (or omit the name parameter to use the helper’s random default) to keep the test isolated.

Suggested change
const relGlossary = new Glossary('TypedRelations');
const relGlossary = new Glossary(`TypedRelations_${uuid()}`);

Copilot uses AI. Check for mistakes.
Comment on lines +424 to +425
private static final Object SETTINGS_LOCK = new Object();

Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file updates the global glossaryTermRelationSettings and only guards it with a class-local SETTINGS_LOCK. Integration tests run with JUnit parallel execution enabled, and other test classes also mutate these settings, so this can still race across classes. Consider using JUnit’s @ResourceLock with a shared lock name for these settings (and applying it in the other settings-mutating tests) to prevent cross-class interference.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 61%
61.94% (61763/99708) 42.06% (33022/78508) 45.09% (9763/21648)

- Replace naive comma split in findRowByTerm with Apache Commons CSV
  parser so quoted/escaped fields don't shift column indices.
- Replace class-local SETTINGS_LOCK with JUnit @ResourceLock keyed on
  "glossaryTermRelationSettings" so settings mutations serialise across
  test classes, and document the contract for future authors.
- Add uuid() suffixes to Playwright glossary names to avoid collisions
  under fullyParallel test execution.
- Guard download.createReadStream() against null and use a waiting
  toContainText assertion on the error cell so a missing selector
  surfaces a clearer failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (17 flaky)

✅ 3958 passed · ❌ 0 failed · 🟡 17 flaky · ⏭️ 86 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 297 0 2 4
🟡 Shard 2 755 0 4 8
🟡 Shard 3 730 0 2 7
🟡 Shard 4 758 0 1 18
🟡 Shard 5 686 0 1 41
🟡 Shard 6 732 0 7 8
🟡 17 flaky test(s) (passed on retry)
  • Features/DataAssetRulesEnabled.spec.ts › should enforce single domain selection for glossary term when entity rules are enabled (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/ActivityAPI.spec.ts › Activity event is created when description is updated (shard 2, 1 retry)
  • Features/ActivityAPI.spec.ts › Activity event shows the actor who made the change (shard 2, 1 retry)
  • Features/ColumnBulkOperations.spec.ts › should clear individual filter and update URL (shard 2, 1 retry)
  • Features/DomainFilterQueryFilter.spec.ts › Domain filter should persist across page navigation (shard 2, 1 retry)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 1 retry)
  • Flow/PersonaDeletionUserProfile.spec.ts › User profile loads correctly before and after persona deletion (shard 3, 1 retry)
  • Pages/Entity.spec.ts › Tier Add, Update and Remove (shard 4, 1 retry)
  • Pages/EntityDataSteward.spec.ts › Tier Add, Update and Remove (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Column dropdown drag-and-drop functionality for Glossary Terms table (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: searchIndex (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Import ODCS with team owner (shard 6, 1 retry)
  • Pages/ServiceEntity.spec.ts › Tier Add, Update and Remove (shard 6, 1 retry)
  • Pages/UserDetails.spec.ts › Admin user can edit teams from the user profile (shard 6, 1 retry)
  • Pages/Users.spec.ts › Create and Delete user (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

…class flakes

GlossaryOntologyExportIT and GlossaryTermRelationIT both flip global
RdfConfiguration in @BeforeAll and write synchronously to a Fuseki
backend. They were marked @execution(ExecutionMode.SAME_THREAD), which
only serialises within the class — other test classes still run
concurrently, inherit the enabled RDF state, and contend for the same
Fuseki backend, causing the export request to time out at 60s.

Add @isolated so JUnit 5 ensures no other class runs while these are
running. This matches the pattern RdfResourceIT already uses for the
same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 27, 2026 03:30
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Apr 27, 2026

Code Review ✅ Approved 3 resolved / 3 findings

Expands glossary CSV test coverage across unit, IT, and Playwright suites. Resolved issues include naive comma parsing, cross-class settings synchronization, and brittle selector usage.

✅ 3 resolved
Bug: findRowByTerm uses naive comma split, breaks on quoted CSV fields

📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryCsvRelationTypesIT.java:596-604
The helper findRowByTerm splits rows with lines[i].split(",", -1) to locate the name field at index 1. However, the server's export endpoint uses Apache Commons CSV which produces RFC 4180–compliant output — meaning fields containing commas, newlines, or quotes are wrapped in double-quotes. If a parent field (index 0) ever contains a comma (e.g. a deeply nested FQN), the naive split shifts all column indices and the lookup silently fails, returning null.

Currently the tests use simple term names so this won't bite immediately, but this is fragile and will cause false-negative test failures the moment test data evolves. Since this is a helper used by 4+ test methods, it's worth fixing now.

Edge Case: SETTINGS_LOCK only guards within this class, not cross-class

📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryCsvRelationTypesIT.java:424
The SETTINGS_LOCK object (line 424) serialises access to the global glossaryTermRelationSettings endpoint, but only within this test class. If any other IT class also modifies that setting concurrently (now or in the future), the lock won't help. This is acceptable for now since this is the only class touching that endpoint, but consider adding a comment documenting the constraint so future test authors know to coordinate.

Edge Case: Playwright test uses .rdg-cell-details selector without fallback

📄 openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts:909-914
In the negative Playwright test (line 910-911), firstRow.locator('.rdg-cell-details').textContent() will throw if the UI ever restructures the error display. The selector is tightly coupled to the react-data-grid internal class name. If rdg-cell-details doesn't appear (e.g. the error renders differently), the test will fail with a confusing timeout rather than a clear assertion message. Consider adding a waitFor or a more resilient data-testid based selector.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Comment on lines +468 to +473
Map<String, String> typeByTermId =
imported.getRelatedTerms().stream()
.collect(
Collectors.toMap(
r -> r.getTerm().getId().toString(), TermRelation::getRelationType));
assertEquals("synonym", typeByTermId.get(t1.getId().toString()), "t1 should be synonym");
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In testImportPreservesMixedRelationsViaApi, Collectors.toMap(..., TermRelation::getRelationType) will throw a NullPointerException if any relationType is null (e.g., if the importer regresses to omitting the default). This makes failures less actionable. Consider building the map imperatively (or mapping through a non-null accessor) so the assertion fails with a clear message instead of an NPE.

Copilot uses AI. Check for mistakes.
Comment on lines +543 to +547
Map<String, String> typeByTermId =
clone.getRelatedTerms().stream()
.collect(
Collectors.toMap(
r -> r.getTerm().getId().toString(), TermRelation::getRelationType));
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: Collectors.toMap(..., TermRelation::getRelationType) can NPE on null values, which obscures whether the failure was due to a missing default relatedTo type. Prefer collecting in a way that tolerates nulls so the test fails via an explicit assertion with context.

Copilot uses AI. Check for mistakes.
Comment on lines +606 to +616
* (e.g. a deeply nested parent FQN containing a comma) don't shift column indices and break the
* lookup. Returns the original line text so callers can run substring assertions against it.
*/
private String findRowByTerm(String csvContent, String termName) throws Exception {
String[] lines = csvContent.split("\\R");
try (CSVParser parser =
CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new StringReader(csvContent))) {
for (CSVRecord record : parser) {
if (termName.equals(record.get("name*"))) {
int lineIndex = Math.toIntExact(record.getRecordNumber());
return lineIndex < lines.length ? lines[lineIndex] : null;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findRowByTerm uses record.getRecordNumber() as an index into csvContent.split("\\R"). recordNumber is a record counter, not a physical line number, so this breaks if any exported field contains embedded newlines (valid CSV) or if blank lines are present. A more robust approach is to assert on the parsed CSVRecord columns (e.g., record.get("relatedTerms")) instead of trying to recover the original raw line.

Suggested change
* (e.g. a deeply nested parent FQN containing a comma) don't shift column indices and break the
* lookup. Returns the original line text so callers can run substring assertions against it.
*/
private String findRowByTerm(String csvContent, String termName) throws Exception {
String[] lines = csvContent.split("\\R");
try (CSVParser parser =
CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new StringReader(csvContent))) {
for (CSVRecord record : parser) {
if (termName.equals(record.get("name*"))) {
int lineIndex = Math.toIntExact(record.getRecordNumber());
return lineIndex < lines.length ? lines[lineIndex] : null;
* (e.g. a deeply nested parent FQN containing a comma or embedded newlines) are handled as a
* single logical record. Returns a normalized CSV row reconstructed from the parsed record so
* callers can run substring assertions against it without relying on physical line numbers.
*/
private String findRowByTerm(String csvContent, String termName) throws Exception {
try (CSVParser parser =
CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(new StringReader(csvContent))) {
for (CSVRecord record : parser) {
if (termName.equals(record.get("name*"))) {
return CSVFormat.DEFAULT.format((Object[]) record.values());

Copilot uses AI. Check for mistakes.
Comment on lines +759 to +764
await sidebarClick(page, SidebarItem.GLOSSARY);
await selectActiveGlossary(page, relGlossary.data.displayName);

await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="import-button-description"]');

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test follows the same UI navigation path as earlier specs that include closeFirstPopupAlert(page) to mitigate the intermittent “glossary not found” popup under parallel execution. Consider adding that same safety step after selecting the glossary (before clicking Manage/Import) to avoid flakiness in CI.

Copilot uses AI. Check for mistakes.
Comment on lines +892 to +897
await sidebarClick(page, SidebarItem.GLOSSARY);
await selectActiveGlossary(page, relGlossary.data.displayName);

await page.click('[data-testid="manage-button"]');
await page.click('[data-testid="import-button-description"]');

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same navigation flake risk as the previous typed-relations spec: consider calling closeFirstPopupAlert(page) after selecting the glossary and before opening the import modal, to prevent occasional popup overlays from breaking the import flow when tests run fully parallel.

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link
Copy Markdown

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants