diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md new file mode 100644 index 0000000000..959fd2837f --- /dev/null +++ b/changes/unreleased/use-case-connections.changed.md @@ -0,0 +1 @@ +- The SysML v1 migration writes actors and use cases as the usages a v2 use case model is made of — a UML Actor is a `part` usage, a UseCase a `use case` usage (its subject, includes and behavior inside it) — so each is one element a tool draws once, and writes the lines a use case diagram draws as connections between them: each actor association, «Include», Extend and «Refine» becomes a `connection` named by its kind (`'Operator to Control Instrument'`, `'Control Instrument includes Manipulate Stage'`, `'A extends B'`, `'A refines B'`), hosted by the nearest package holding both ends, with no trailing comment a tool would show as a note; a relationship its author named keeps that name and carries its kind as the connection's `doc` instead, as an extend's extension points and condition are. A refined requirement or block gets one usage in its package only when a connection joins it; a named association's one usage is typed by its `connection def`. Use case diagrams are rendered `asInterconnectionDiagram` over those usages and connections instead of `asTextualNotation`, with their Cameo geometry applied; the interconnection renderer draws use case and requirement usages as nodes and an `include` as the connection's line rather than a nested node. A «Requirement» is a `requirement` usage too, as a tool's users draw one, rather than a `requirement def` with a usage written beside it when a line needs one: a nested requirement is its subrequirement, a «Satisfy» or «Verify» subsets it (`satisfy requirement 'satisfy R' :> R;`), a «DeriveReqt»'s `connection def` ends and a property it types subset it, a «Refine» connection joins it directly, and a requirement table, matrix or «Requirement»-stereotype filter lists the usages by name; an instance specification of a requirement is reported rather than written as an individual. A document query reads a derivation `connection def` whose ends subset requirement usages as relating those usages, and a named satisfy or verify that subsets a requirement usage as satisfying or verifying it too. An «Allocate» whose end is a requirement, actor or use case stays a dependency, as no allocation end can be typed by a usage. diff --git a/client/julia/OpenSysML/test/extended_client.jl b/client/julia/OpenSysML/test/extended_client.jl index e766555404..34562d99b7 100644 --- a/client/julia/OpenSysML/test/extended_client.jl +++ b/client/julia/OpenSysML/test/extended_client.jl @@ -682,12 +682,12 @@ end @test occursin("part def Vehicle", migrated.content) @test migrated.from_format == "xmi" @test (migrated.report.mapped, migrated.report.approximated, - migrated.report.unmapped, migrated.report.skipped) == (78, 12, 3, 2) + migrated.report.unmapped, migrated.report.skipped) == (77, 13, 3, 2) @test length(migrated.report.entries) == 95 @test length(by_verdict(migrated.report, VERDICT_UNMAPPED)) == 3 @test migrated.source_path == abspath(vehicle) inline = migrate_source(conn, read(vehicle), "ttl"; from_format=" XMI ") - @test inline.to_format == "ttl" && inline.report.mapped == 78 + @test inline.to_format == "ttl" && inline.report.mapped == 77 @test isempty(inline.report.entries) && inline.source_path === nothing mktempdir() do directory output = joinpath(directory, "Vehicle.sysml") diff --git a/client/matlab/tests/test_surface_live.m b/client/matlab/tests/test_surface_live.m index f9bb9bb381..952e1ee844 100644 --- a/client/matlab/tests/test_surface_live.m +++ b/client/matlab/tests/test_surface_live.m @@ -68,14 +68,14 @@ function test_surface_live() assert_equal(~isempty(strfind(migrated.content, 'part def Vehicle')), true, 'migrate'); assert_equal(migrated.fromFormat, 'xmi', 'migrate from format'); assert_equal([migrated.report.mapped, migrated.report.approximated, ... - migrated.report.unmapped, migrated.report.skipped], [78 12 3 2], 'migration counts'); + migrated.report.unmapped, migrated.report.skipped], [77 13 3 2], 'migration counts'); assert_equal(numel(migrated.report.entries), 95, 'migration entries'); assert_equal(numel(migrated.byVerdict('unmapped')), 3, 'migration unmapped entries'); assert_equal(migrated.sourcePath, opensysml.internal.absolutePath(vehicle), 'migration source path'); fid = fopen(vehicle, 'rb'); vehicleBytes = fread(fid, Inf, 'uint8=>uint8')'; fclose(fid); inlineMigration = opensysml.migrate(conn, 'ttl', 'content', vehicleBytes, 'fromFormat', ' XMI '); assert_equal(inlineMigration.toFormat, 'ttl', 'inline migration to Turtle'); - assert_equal(inlineMigration.report.mapped, 78, 'inline migration counts'); + assert_equal(inlineMigration.report.mapped, 77, 'inline migration counts'); assert_equal(isempty(inlineMigration.report.entries), true, 'inline migration summary only'); assert_equal(isempty(inlineMigration.sourcePath), true, 'inline migration has no source path'); migratedPath = [tempname '.sysml']; diff --git a/client/node/test/migrate.test.ts b/client/node/test/migrate.test.ts index 4820cd1fce..1154414464 100644 --- a/client/node/test/migrate.test.ts +++ b/client/node/test/migrate.test.ts @@ -44,10 +44,10 @@ test("a v1 model migrates from its path, with the summary and the counts", async ); assert.match( migration.report.summary, - /^migrated 93 element\(s\): 78 mapped, 12 approximated, 3 unmapped/, + /^migrated 93 element\(s\): 77 mapped, 13 approximated, 3 unmapped/, ); - assert.equal(migration.report.mapped, 78); - assert.equal(migration.report.approximated, 12); + assert.equal(migration.report.mapped, 77); + assert.equal(migration.report.approximated, 13); assert.equal(migration.report.unmapped, 3); assert.equal(migration.report.skipped, 2); assert.notEqual(migration.report.source, ""); diff --git a/client/python/opensysml/__init__.py b/client/python/opensysml/__init__.py index 9be872322b..325be51e18 100644 --- a/client/python/opensysml/__init__.py +++ b/client/python/opensysml/__init__.py @@ -377,7 +377,7 @@ def migrate(to_format, file_path=None, content=None, from_format='', report=Fals >>> import opensysml >>> migrated = opensysml.migrate("sysml", file_path="Vehicle.mdzip", report=True) >>> migrated.report.summary - 'migrated 93 element(s): 78 mapped, 12 approximated, 3 unmapped (...)' + 'migrated 93 element(s): 77 mapped, 13 approximated, 3 unmapped (...)' >>> migrated.write("Vehicle.sysml") 'Vehicle.sysml' """ diff --git a/client/rust/opensysml/tests/surface.rs b/client/rust/opensysml/tests/surface.rs index f47cfcad0e..c1f12f04d0 100644 --- a/client/rust/opensysml/tests/surface.rs +++ b/client/rust/opensysml/tests/surface.rs @@ -292,7 +292,7 @@ fn a_v1_model_is_migrated_and_refused_by_convert() { migrated.report.unmapped, migrated.report.skipped ), - (78, 12, 3, 2) + (77, 13, 3, 2) ); assert_eq!(migrated.report.entries.len(), 95); assert_eq!(migrated.report.by_verdict("unmapped").len(), 3); @@ -315,7 +315,7 @@ fn a_v1_model_is_migrated_and_refused_by_convert() { assert_eq!(inline.to_format, "ttl"); assert!(inline.source_path.is_none()); assert!(inline.report.entries.is_empty()); - assert_eq!(inline.report.mapped, 78); + assert_eq!(inline.report.mapped, 77); let refused = connection.convert( "sysml", diff --git a/conformance/scenarios/16-migrate.json b/conformance/scenarios/16-migrate.json index 4615efc557..a322f1c935 100644 --- a/conformance/scenarios/16-migrate.json +++ b/conformance/scenarios/16-migrate.json @@ -29,7 +29,7 @@ "experimental": true }, "contains": { - "report.summary": "migrated 93 element(s): 78 mapped, 12 approximated, 3 unmapped" + "report.summary": "migrated 93 element(s): 77 mapped, 13 approximated, 3 unmapped" }, "non_empty": [ "experimental_notice", @@ -63,8 +63,8 @@ "response": { "from_format": "xmi", "report": { - "mapped": 78, - "approximated": 12, + "mapped": 77, + "approximated": 13, "unmapped": 3, "skipped": 2 } diff --git a/docs/guide/11-migrating-from-sysml-v1.md b/docs/guide/11-migrating-from-sysml-v1.md index 7c6b6c704d..04fb064298 100644 --- a/docs/guide/11-migrating-from-sysml-v1.md +++ b/docs/guide/11-migrating-from-sysml-v1.md @@ -99,7 +99,7 @@ need attention first: ## approximated (11) «Trace» Abstraction Requirements:: _dep_trace (a trace is written as a plain dependency) «TestCase» Activity Requirements::Mass Test _tc_mass -> Requirements::'Mass Test' (the test case's behavior is not migrated; only its verified requirements are) -Actor Vehicle Design::Driver _actor_driver -> 'Vehicle Design'::Driver (a UML actor is written as a part def) +Actor Vehicle Design::Driver _actor_driver -> 'Vehicle Design'::Driver (a UML actor is written as a part usage) «FlowPort» Port Vehicle Design::Engine::speedIn _port_speedIn -> 'Vehicle Design'::Engine::speedIn (a port typed by a DataType is written as a port holding one directed attribute) … Property Vehicle Design::Vehicle::totalMass _prop_total -> 'Vehicle Design'::Vehicle::totalMass (opaque expression copied verbatim (language SysML)) @@ -119,7 +119,7 @@ how, or why not. The columns are tab-separated, so `cut` and `awk` read them. chosen (the root model's members are written at the top level; an English constraint body was translated to a v2 expression). - **approximated** — written as the nearest v2 construct, and the note says what was lost. Some - of these are simply how v2 spells the idea (a UML actor is a `part def`; a flow port typed by a + of these are simply how v2 spells the idea (a `«Trace»` is a `dependency`; a flow port typed by a data type is a port holding one directed attribute; a `«Trace»` is a `dependency`). Others record a v1 tag that v2 has no home for (`isEncapsulated`, a value type's `unit` and `quantityKind`), or an opaque expression copied verbatim because its declared language is diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index cf6bde5602..9a2b875ded 100644 --- a/docs/project/sysml-v1-transformation-census-baseline.json +++ b/docs/project/sysml-v1-transformation-census-baseline.json @@ -22,7 +22,7 @@ }, { "name": "fixtures", - "files": 101 + "files": 103 } ], "counts": { @@ -98,6 +98,10 @@ "pssm": 0, "fixtures": 1 }, + "uml:Actor": { + "pssm": 0, + "fixtures": 8 + }, "uml:AddStructuralFeatureValueAction": { "pssm": 525, "fixtures": 18 @@ -136,7 +140,7 @@ }, "uml:Class": { "pssm": 311, - "fixtures": 488 + "fixtures": 490 }, "uml:ClearAssociationAction": { "pssm": 0, @@ -244,7 +248,7 @@ }, "uml:Property": { "pssm": 25, - "fixtures": 466 + "fixtures": 468 }, "uml:Property[qualifier]": { "pssm": 0, @@ -348,7 +352,7 @@ }, "uml:UseCase": { "pssm": 0, - "fixtures": 10 + "fixtures": 12 }, "uml:ValueSpecificationAction": { "pssm": 1061, @@ -20987,16 +20991,18 @@ ], "operations": [], "ocl": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorMember", + "internal/translate/migrate/usecase.go:actorLink", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages", + "scope": [ + "uml:Actor" + ] }, { "name": "IncludeFeatureTyping_Mapping", @@ -21063,16 +21069,18 @@ "type" ], "ocl": "sha256:719917ab2ec0a4e8566e0084ac378f0182dd8f3ed98522ab6c780ed6a5001349", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorMember", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseActorMembership_Mapping", @@ -21089,16 +21097,18 @@ "ownedMemberParameter" ], "ocl": "sha256:350dd57e7124d89991e86930a62206324b4913cc22f5dfbc212c7fb53353cc02", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorMember", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseActor_Mapping", @@ -21116,16 +21126,18 @@ "ownedRelationship" ], "ocl": "sha256:a3c87cbca08a5c2087397677178ad9e15063e3bad7ab80893904b89e3c79001c", - "status": "faithful", + "status": "approximate", "implementation": [ - "internal/translate/migrate/usecase.go:actorMember", + "internal/translate/migrate/usecase.go:connectActor", "internal/translate/migrate/classify.go:classifyClass" ], "tests": [ "tests/migrate/migrate_test.go:TestGoldenNotation" ], - "reason": "", - "scope": [] + "reason": "the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage", + "scope": [ + "uml:Actor" + ] }, { "name": "UseCaseEmptySubjectReferenceUsage_Mapping", diff --git a/docs/project/sysml-v1-transformation-census.md b/docs/project/sysml-v1-transformation-census.md index 22a6f3c09b..41dd426c23 100644 --- a/docs/project/sysml-v1-transformation-census.md +++ b/docs/project/sysml-v1-transformation-census.md @@ -34,7 +34,7 @@ that build one relationship of a target — share the verdict of the construct t -**Census:** 783 mapping classes — 505 ✅ faithful, 45 ⚠️ approximate, 42 ❌ not implemented, 191 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. +**Census:** 783 mapping classes — 501 ✅ faithful, 49 ⚠️ approximate, 42 ❌ not implemented, 191 ⛔ deliberate, 0 🚧 known failure, 0 ❔ unknown. | Package | Mappings | ✅ | ⚠️ | ❌ | ⛔ | 🚧 | ❔ | |---|---|---|---|---|---|---|---| @@ -65,9 +65,9 @@ that build one relationship of a target — share the verdict of the construct t | UML4SysML::SimpleClassifiers | 22 | 20 | 0 | 0 | 2 | 0 | 0 | | UML4SysML::StateMachines | 26 | 22 | 4 | 0 | 0 | 0 | 0 | | UML4SysML::StructuredClassifiers | 39 | 15 | 0 | 0 | 24 | 0 | 0 | -| UML4SysML::UseCases | 14 | 10 | 0 | 4 | 0 | 0 | 0 | +| UML4SysML::UseCases | 14 | 6 | 4 | 4 | 0 | 0 | 0 | | UML4SysML::Values | 33 | 14 | 0 | 3 | 16 | 0 | 0 | -| **Total** | 783 | 505 | 45 | 42 | 191 | 0 | 0 | +| **Total** | 783 | 501 | 49 | 42 | 191 | 0 | 0 | ## Census rows @@ -948,12 +948,12 @@ that build one relationship of a target — share the verdict of the construct t | OMG mapping | v1 source | v2 target | Implementation | Test | Status | Reason | |---|---|---|---|---|---|---| -| `Actor_Mapping` | `Actor` | `PartDefinition` | `internal/translate/migrate/usecase.go:actorMember`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | +| `Actor_Mapping` | `Actor` | `PartDefinition` | `internal/translate/migrate/usecase.go:actorLink`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages | | `IncludeFeatureTyping_Mapping` | `Include` | `FeatureTyping` | `internal/translate/migrate/usecase.go:include` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | | `Include_Mapping` | `Include` | `IncludeUseCaseUsage` | `internal/translate/migrate/usecase.go:include` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActorFeatureTyping_Mapping` | `Property` | `FeatureTyping` | `internal/translate/migrate/usecase.go:actorMember`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActorMembership_Mapping` | `Property` | `ActorMembership` | `internal/translate/migrate/usecase.go:actorMember`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | -| `UseCaseActor_Mapping` | `Property` | `PartUsage` | `internal/translate/migrate/usecase.go:actorMember`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ✅ faithful | — | +| `UseCaseActorFeatureTyping_Mapping` | `Property` | `FeatureTyping` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| `UseCaseActorMembership_Mapping` | `Property` | `ActorMembership` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| `UseCaseActor_Mapping` | `Property` | `PartUsage` | `internal/translate/migrate/usecase.go:connectActor`
`internal/translate/migrate/classify.go:classifyClass` | `tests/migrate/migrate_test.go:TestGoldenNotation` | ⚠️ approximate | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | | `UseCaseEmptySubjectReferenceUsage_Mapping` | `UseCase` | `ReferenceUsage` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | | `UseCaseObjectiveMembership_Mapping` | `UseCase` | `ObjectiveMembership` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | | `UseCaseObjectiveRequirementUsage_Mapping` | `UseCase` | `RequirementUsage` | — | — | ❌ not implemented | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | @@ -1099,11 +1099,13 @@ SysML profile namespace. The counts are elements affected, not effort. | 7 | 3: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | these are used only by TIAOperatorExpression_Mapping, as the private first/second operands of a TestIdentityAction's `==`, not by Expression trees; they are written with TestIdentityAction, which is not migrated (PSSM: 14) | | 8 | 3: `TIAOperatorExpression_Mapping`, `TIAResultExpressionMembership_Mapping`, `TestIdentityAction_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | a TestIdentityAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | | 9 | 9: `RICOAFeatureValueOperatorExpressionFeatureValue_Mapping`, `RICOAFeatureValueOperatorExpressionFeature_Mapping`, `RICOAFeatureValueOperatorExpression_Mapping`, `RICOAFeatureValueOperatorFeatureReferenceExpression_Mapping`, `RICOAFeatureValueOperatorMembership_Mapping`, `RICOAFeatureValueOperatorParameterMembership_Mapping`, `RICOAFeatureValue_Mapping`, `RICOAOutputPin_Mapping`, `ReadIsClassifiedObjectAction_Mapping` | ❌ not implemented | `uml:ReadIsClassifiedObjectAction` | 12 | 0 | 12 | a ReadIsClassifiedObjectAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 10 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 9 | 11 | a decider loop whose parts exchange values over pins the migrator writes is written as a v2 loop (the loops.xmi tested-first and post-tested fixtures): its loop variables and an `ended` flag as private features, its parts performing in the loop — test before body when tested first, else after — and its results read from the variables at the end; no corpus loop takes the loop form: PSSM's 2 fall back because their body outputs are structured-node output pins the migrator does not write, and TMT's 1 names no decider, so each keeps the body-once form and stays approximated per element; candidate erratum: the mapping gives only an ActionUsage and leaves its details undefined, so a loop's test and iteration are lost | -| 11 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 10 | 10 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | -| 12 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | -| 13 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | -| 14 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | -| 15 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | -| 16 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | +| 10 | 4: `UseCaseEmptySubjectReferenceUsage_Mapping`, `UseCaseObjectiveMembership_Mapping`, `UseCaseObjectiveRequirementUsage_Mapping`, `UseCaseObjectiveSubjectMembership_Mapping` | ❌ not implemented | `uml:UseCase` | 0 | 12 | 12 | no objective is written for a use case: v1 has none to read, and the empty objective requirement the mapping creates is not written | +| 11 | 1: `LoopNode_Mapping` | ⚠️ approximate | `uml:LoopNode` | 2 | 9 | 11 | a decider loop whose parts exchange values over pins the migrator writes is written as a v2 loop (the loops.xmi tested-first and post-tested fixtures): its loop variables and an `ended` flag as private features, its parts performing in the loop — test before body when tested first, else after — and its results read from the variables at the end; no corpus loop takes the loop form: PSSM's 2 fall back because their body outputs are structured-node output pins the migrator does not write, and TMT's 1 names no decider, so each keeps the body-once form and stays approximated per element; candidate erratum: the mapping gives only an ActionUsage and leaves its details undefined, so a loop's test and iteration are lost | +| 12 | 1: `Actor_Mapping` | ⚠️ approximate | `uml:Actor` | 0 | 8 | 8 | an actor is written as a part usage, not a part def: the actor lines of a use case diagram are connections, which join usages | +| 13 | 3: `UseCaseActorFeatureTyping_Mapping`, `UseCaseActorMembership_Mapping`, `UseCaseActor_Mapping` | ⚠️ approximate | `uml:Actor` | 0 | 8 | 8 | the use case owns no actor member: the association is written as a connection between the actor's part usage and the use case usage | +| 14 | 2: `TestCaseActivityReturnParameterMembership_Mapping`, `TestCaseActivity_Mapping` | ⚠️ approximate | `sysml:TestCase` | 0 | 5 | 5 | the test case is written as a `verification def` whose objective verifies the requirements, but its behavior (activity or interaction) is not migrated | +| 15 | 8: `ElementGroupMetadaMembership_Mapping`, `ElementGroupMetadataFeatureMembership_Mapping`, `ElementGroupMetadataFeatureTyping_Mapping`, `ElementGroupMetadataFeatureValue_Mapping`, `ElementGroupMetadataRedefinition_Mapping`, `ElementGroupMetadataReferenceUsage_Mapping`, `ElementGroupMetadataUsage_Mapping`, `ElementGroup_Mapping` | ❌ not implemented | `sysml:ElementGroup` | 0 | 0 | 0 | «ElementGroup» is not read by the migrator: the comment is written as a comment and no package grouping its members is written | +| 16 | 8: `ProblemRationaleMetadataFeatureMembership_Mapping`, `ProblemRationaleMetadataFeatureTyping_Mapping`, `ProblemRationaleMetadataFeatureValue_Mapping`, `ProblemRationaleMetadataMembership_Mapping`, `ProblemRationaleMetadataRedefinition_Mapping`, `ProblemRationaleMetadataReferenceUsage_Mapping`, `ProblemRationaleMetadataUsage_Mapping`, `ProblemRationale_Mapping` | ❌ not implemented | `sysml:Problem`
`sysml:Rationale` | 0 | 0 | 0 | «Problem» and «Rationale» are not read by the migrator: the comment is written as a comment without metadata saying which it is | +| 17 | 7: `REAFeatureValueOperatorExpressionFeatureTyping_Mapping`, `REAFeatureValueOperatorExpressionFeature_Mapping`, `REAFeatureValueOperatorExpressionMembership_Mapping`, `REAFeatureValueOperatorExpression_Mapping`, `REAFeatureValue_Mapping`, `REAOutputPin_Mapping`, `ReadExtentAction_Mapping` | ❌ not implemented | `uml:ReadExtentAction` | 0 | 0 | 0 | a ReadExtentAction is not migrated: it is written as a placeholder action that keeps its pins and position in the flow and is reported unmapped | +| 18 | 8: `TraceAnnotation_Mapping`, `TraceMetadataFeatureMembership_Mapping`, `TraceMetadataReferenceUsageFeatureValue_Mapping`, `TraceMetadataReferenceUsageRedefinition_Mapping`, `TraceMetadataReferenceUsage_Mapping`, `TraceMetadataUsageFeatureTyping_Mapping`, `TraceMetadataUsage_Mapping`, `Trace_Mapping` | ⚠️ approximate | `sysml:Trace` | 0 | 0 | 0 | a «Trace» is written as a plain dependency with a `/* «Trace» */` comment, not as a dependency annotated by trace metadata | diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 1ac8bd2cc1..decc93f4df 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -131,13 +131,13 @@ ties each of its mapping classes to the code that carries it out (or records why | Model, Package | `package` | mapped | | «Block» Class | `part def` | mapped | | Plain UML Class | `occurrence def` | mapped | -| Actor | `part def` | approximated | +| Actor | `part` usage — a role a connection can join, which v2 writes as a usage rather than a definition; an association to it is a `connection` to the use case | mapped | | «InterfaceBlock» | `port def` | mapped | | «ValueType» DataType, PrimitiveType | `attribute def` (`Real`/`Integer`/`Boolean`/`String` for the SysML primitives) | mapped | | Signal | `item def`; properties typed by it are `item` / `ref item` | mapped | | Enumeration and its literals | `enum def` | mapped | | «ConstraintBlock» | `constraint def` with its parameters | mapped | -| «Requirement», «AbstractRequirement» | `requirement def ` with `doc` holding the text; any tool-specific requirement kind applied beside it as a comment | mapped | +| «Requirement», «AbstractRequirement» | `requirement ` usage with `doc` holding the text (a usage, as a tool's users draw one, so a satisfy, verify or refine joins it directly and a nested requirement is a subrequirement); any tool-specific requirement kind applied beside it as a comment | mapped | | «TestCase» | `verification def` | approximated: its behavior is not migrated | | InstanceSpecification of a block, of a constraint block | `individual part def`, `individual constraint def`, with its slots | mapped | | InstanceSpecification of an interface block | `individual def` (v2 has no individual port def); an instance classified by an interface block beside a block is an `individual part def` of the block alone | mapped | @@ -145,6 +145,7 @@ ties each of its mapping classes to the code that carries it out (or records why | Slot of a part, item or constraint property holding one instance | `individual part :>> x : 'the instance';` — `ref` when the property is | mapped | | Slot of a part, item or constraint property holding several instances | `part :>> x [n];` then one `individual part : 'the instance' :> x;` each | mapped | | InstanceSpecification of a value type | `attribute` typed by it, holding its slot values (an individual cannot specialize an attribute def) | mapped | +| InstanceSpecification of an actor or of a use case | not written: the classifier is a `part` or `use case` usage once migrated, which an individual cannot specialize | unmapped | | InstanceSpecification naming no classifier, under a `SimulationConfig`'s `resultLocation`, whose slots are of features of one lineage of blocks ending in the configuration's target classifier or a general of it (a simulation tool's result snapshot) | the `individual part def` of the most special of those blocks, with its slots; the note says which owner classified it and for which configuration | mapped | | InstanceSpecification naming no classifier, anywhere else, or under a `resultLocation` with slots of features of blocks that are no one lineage or none the target is of | comment | **unmapped** — nothing classifies it; under a `resultLocation` the note says which owners its slots have and why they type no snapshot | | Slot contradicting its feature (more values than the multiplicity allows, a repeated value of a feature written unique — declared so in v1, or written without its `nonunique` by the rule above — a feature of a classifier the instance is not written to specialize, an instance that is not of the property's type or of its default individual, a value outside the document) | comment | **unmapped** | @@ -154,7 +155,7 @@ ties each of its mapping classes to the code that carries it out (or records why | Real literal on an `Integer`/`Natural` feature, numeric string on a scalar feature | converted to the feature's scalar | mapped | | Constraint whose specification is a literal, instance or opaque body yielding no Boolean (an integer, a real, a string spelling no `true`/`false`, an enumeration literal) | comment naming the value and the Boolean the constraint yields | **unmapped** — no v2 checker accepts a constraint body of another type; a string `"true"`/`"false"` is written as the Boolean it spells | | Constraint whose specification is a `uml:Expression` tree with no symbol at any node and, as leaves, only `InstanceValue`s naming no instance (a tool's presentation constraint on a document, a Cameo Collaborator marker) | nothing: the tree spells nothing | skipped — notation only; a tree with a symbol, or a leaf naming an instance, is translated or refused like any other expression | -| Association with a name, «AssociationBlock» | `connection def` | mapped | +| Association with a name, «AssociationBlock» | `connection def`; an association to an Actor also has its one usage, `connection : connect to ;` between the actor's `part` usage and the `use case` usage, hosted by the nearest package holding both | mapped | | Anonymous association with a classifier-owned end | nothing: the end property carries it | mapped | | Anonymous association owning every end | a named `connection def` | approximated | | Value property | `attribute`, with multiplicity and default | mapped | @@ -175,13 +176,13 @@ ties each of its mapping classes to the code that carries it out (or records why | Connector, nested ends | `connect a.b to c.d` | mapped | | «BindingConnector» | `bind`, or `binding name bind` when named | mapped | | InformationFlow / «ItemFlow» over a connector | `flow of Item from a.x to b.y`, from the end standing for the flow's source to the one standing for its target: the end whose role it is, whose path names it or a part typed by it (a flow a tool draws between parts or blocks), or the connector's own port when it is the connector's owner | mapped | -| «Satisfy» | `satisfy requirement … by …` in the satisfying usage's owner | mapped | +| «Satisfy» | `satisfy requirement … :> by …` in the satisfying usage's owner | mapped | | «Verify» from a test case | `verify` in the verification def | mapped | | «DeriveReqt» | `connection … :> RequirementDerivation::Derivation` | mapped | | «Allocate» | between different bodies, an `allocation def 'A to B'` whose ends redefine `source` and `target`, typed by the definitions that own the endpoints; a feature endpoint is chained in `allocate source.x to target.y`, and a definition endpoint is just `source` or `target`. When both endpoints are features of one definition body, or both are package features, it stays `allocate a to b` in that body. A feature owned by a package cannot type an allocation end and keeps a noted dependency | mapped, except for an endpoint owned by a package that needs the noted dependency fallback | | «Allocate» whose feature endpoint has no enclosing written definition, such as a package-owned feature paired with an endpoint from another body | `dependency a to b`, with a note naming the endpoint the allocation ends cannot type | approximated | | «Allocate», or another dependency, whose end is an activity node written only as a placeholder (a call that is not migrated) | the relationship is written to the placeholder; the pair ending there counts as failed when its end is not migrated, so the note gives the final tally of pairs written and names the end | approximated when another pair is written, **unmapped** when none is | -| «Refine» | `dependency` carrying `@ModelingMetadata::Refinement` | mapped | +| «Refine» | `connection ' refines ' connect to ;` between usages of its ends (an actor's `part`, a `use case`, a `requirement`, or a `part`, `action`, `occurrence` or `item` usage written for the definition in its package), other metadata in its body — a line a diagram draws, named by its kind since the standard `Refinement` metadata annotates only a dependency and a comment would be a note. A «Refine» whose end is no such element stays a `dependency` carrying `@ModelingMetadata::Refinement` | approximated | | «Trace», «Copy», other stereotyped dependencies | plain `dependency` with the stereotype as a comment; named relationships keep their name | approximated | | A user stereotype specializing a standard one («Org Requirement» :> «Requirement», or one specializing «Block», «ValueType», «Satisfy», «Verify», «Refine», «Trace», «DeriveReqt», «Allocate», …) | the standard stereotype's v2 form above, its tags read as the standard ones (`Id`, `Text`, …), plus a `@Profile::'Org Requirement' { … }` usage holding the user-added tags | as the standard form | | Comment, Documentation | `doc` (first) / `comment`, HTML tags stripped and the cross-references in it read as plain text ([below](#cross-references-in-documentation)) | mapped (approximated when a reference is dangling or stale) | @@ -204,11 +205,11 @@ ties each of its mapping classes to the code that carries it out (or records why | InterfaceRealization from a block | a `port` of the `part def` typed by the interface's `port def` — reused when the block already owns one so typed, otherwise added under the interface's name; a `part def` cannot specialize a `port def` | approximated | | InterfaceRealization from an «InterfaceBlock» | `port def :> ` | mapped | | InterfaceRealization whose interface is not written (outside the document, library content) or whose client becomes neither a part def nor a port def | comment naming why | **unmapped** | -| UseCase (whatever incidental stereotype a tool applies to it) | `use case def`; its UML `subject` is a `subject` usage (v2 admits one per case: a second is a `ref part` with a note); an anonymous association to an Actor is an `actor` of the use case typed by the actor's `part def`, with the association's multiplicity; a `classifierBehavior` is performed as in a block | mapped | -| Include | `include use case : ;` | mapped | -| Extend, ExtensionPoint | `dependency to ;` in the extending case, the extension points and condition as a comment; v2 has no `extend` | approximated / **unmapped** (extension point) | +| UseCase (whatever incidental stereotype a tool applies to it) | `use case` usage — one element, as in v2 use case models, which a connection can join; its UML `subject` is a `subject` usage (v2 admits one per case: a second is a `ref part` with a note); an anonymous association to an Actor is a `connection ' to ' connect to ;` in the nearest package holding both, which a use case diagram draws; an actor end the use case owns is its `actor`, subsetting the actor's `part`, with the association's multiplicity; a `classifierBehavior` is performed as in a block | mapped | +| Include | `include ;` in the including case and `connection ' includes ' connect to ;` between the two cases, which a diagram draws | mapped | +| Extend, ExtensionPoint | `connection ' extends ' connect to ;` between the two cases, the extension points and condition as the connection's `doc`; v2 has no `extend` | approximated / **unmapped** (extension point) | | Include, Extend whose other case is not in the document | comment | **unmapped** | -| Property typed by a UseCase | `ref use case x : ;` | approximated | +| Property typed by a UseCase | `ref use case x :> ;` | approximated | | «View» Class | package-level `view ` usage (v2 admits `expose` in a usage alone): `satisfy ` for its `viewpoint` tag and its «Conform» generalizations and dependencies; `expose` members for its «Expose» dependencies; its «View» property typed by another view a nested `view x :> ;`, `ref` when not composite | mapped | | «View» Package | `view ` usage holding the package's members | approximated | | «View» whose `viewpoint` tag or «Conform» names a viewpoint that is not written, or a view a nested view's feature of an inaccessible definition | the view without that `satisfy`/subsetting, the reason in the report | approximated | @@ -381,7 +382,8 @@ and `umlType` (`Class Diagram`) together — by the first family below a word of | internal block, parametric, composite structure and interconnection diagrams | `Views::asInterconnectionDiagram`; a parametric diagram of a block of the [Monte Carlo pattern](#monte-carlo-analyses) exposes the block's analysis def in place of the pattern's `Mean` symbol and the def's returns in place of its other statistics, the `Mean` binding routed along the def's `observed` | | block definition, class, package, object, component, deployment, profile and other structure diagrams | `Views::asTreeDiagram` | | an activity diagram whose owner is written as an `action def`, a state machine (or statechart) diagram whose owner is written as a `state def`, showing a node or edge of its graph | `view : StandardViewDefinitions::ActionFlowView` / `StateTransitionView`, rendered `Views::asInterconnectionDiagram` | -| other behavior diagrams (sequence, use case, an activity diagram of a package or one showing nothing of its activity's graph), requirement, content and free-form diagrams, a tool's own kinds, a diagram naming no kind | `Views::asTextualNotation` | +| a use case diagram | `Views::asInterconnectionDiagram` over the actors' `part` usages, the `use case` and `requirement` usages and the connections written for the actor associations, includes, extends and «Refine»s it shows | +| other behavior diagrams (sequence, an activity diagram of a package or one showing nothing of its activity's graph), requirement, content and free-form diagrams, a tool's own kinds, a diagram naming no kind | `Views::asTextualNotation` | The rendering is written `$::Views::…` where a member named `Views` would shadow the library, a view definition `$::StandardViewDefinitions::…` where one named `StandardViewDefinitions` would, and a @@ -414,10 +416,10 @@ otherwise a name spelled from what it is written between, in the body it is writ | Transition | `transition 'S accept Sig then T' first S accept Sig then T;` — the trigger, guard and target as written, the payload binding left out of the name; several triggers are several transitions, each named for its own trigger (a v1 name is numbered, `halt`, `halt2`), and the edge's route pins every one of them | | Connector | `connection 'a.p to b.q' connect a.p to b.q;` | | BindingConnector, delegation connector | `binding 'a.p = b.q' bind a.p = b.q;` | -| Dependency, Extend | `dependency 'A to B' from A to B;` (`allocation` for an «Allocate»); several clients or suppliers are several dependencies, one per pair, and the view exposes each | -| «Satisfy» | `satisfy requirement 'satisfy R' : R;` in the satisfying usage's owner, one per client in its own owner's body, and the view exposes each | -| «Verify» | `verify requirement 'verify R' : R;` in the test case's `objective`, which is named `objective` so the member can be qualified; one per pair, as for a «Satisfy» | -| Include, Message | already named members: `include use case x : X;`, the interaction step `action x …` | +| Dependency, Extend, «Refine» | `dependency 'A to B' from A to B;` (`allocation` for an «Allocate»; `connection 'A extends B' connect A to B;` for an Extend, `connection 'A refines B' connect A to B;` for a «Refine» between usages; a relationship its author named keeps that name, and the connection's `doc` is its kind, `doc /* A refines B */`); several clients or suppliers are several dependencies, one per pair, and the view exposes each | +| «Satisfy» | `satisfy requirement 'satisfy R' :> R;` in the satisfying usage's owner, one per client in its own owner's body, and the view exposes each | +| «Verify» | `verify requirement 'verify R' :> R;` in the test case's `objective`, which is named `objective` so the member can be qualified; one per pair, as for a «Satisfy» | +| Include, Message | `connection 'A includes B' connect A to B;` for an Include (a named one keeps its name, the kind its `doc`); the interaction step `action x …` is an already named member | The name is a spelling, not a value: it derives from the ends' written names, never from ids or hashes, so it is stable across runs and readable in the view (`expose 'Wait accept QueryCompleted @@ -608,7 +610,7 @@ metadata, and a look-alike application from an unbundled profile stays a comment | `excludedElements` | `Except(source = , exclude = Named(qualifiedName = (…)))` ahead of the sort, the row noted with their count; an excluded element that resolves to nothing, or that the migration does not write, is absent regardless and the note says so; the rows nested under an excluded row stay, as they do in the tool, which lists each row it hides by itself | | `displayMode` — `List`, `Compact tree` or `Complete tree`, the literals of the MagicDraw profile's `TableDisplayMode` enumeration — with `showScopeAsRoot` and `expandedRows` | a list stays flat; `Compact tree` wraps the sorted rows in `Tree(source = )`, which nests each row under the nearest row containing it at the depth the renderers indent by ([hierarchical rows](../manual/outputs.md#hierarchical-rows)), so a nested row's `name` cell is the element's own name (a dotted name the tool gave a nested instance stays as written) and its depth is structural, never spaces in the name; `Complete tree` adds `ancestors = Descendants(source = )`, so the scope's elements containing the rows join as intermediate levels; `showScopeAsRoot = true` adds the scope itself to the ancestors, as the root (noted, not applied, for a flat list or a table naming no scope). A mode the profile does not define lists the rows flat with the note; `expandedRows` records which nodes the tool had unfolded (`NoExpanded` when none was), which is window state, so every nested row is listed, with the note; an entry not of the form `,` is dropped with the note, not a refusal | | an instance table's `classifiers` | `WhereType(type = ())`, then `WhereType(type = "Definition")` and `WhereFeature('feature' = "isIndividual", operator = "=", value = "true")`, so the rows are the individual definitions the instance specifications became — of the classifier and, as in Cameo, of its subtypes — and not the slots typed by them; `includeSubtypesOfRowTypes = false` is approximated with the note that subtypes are listed too | -| a generic table's `rowElementType` — a UML metaclass or a stereotype | `WhereType` on the v2 kind the metaclass or a standard stereotype [maps to](#mapping) (`Class` and «Block» → `PartDefinition`, «Requirement» → `RequirementDefinition`…); the abstract metaclasses list what they hold in UML, so `Type` and `Classifier` are every `Definition` plus the `ViewUsage`/`ViewpointUsage` a «View»/«Viewpoint» class became, `Namespace` adds `Package` and `StateUsage`, and `PackageableElement` adds `Package` and the dependencies — never the features a classifier owns; `Element` and `NamedElement` alone admit everything; a user stereotype the migration writes as a `metadata def` → `WhereMetadata('metadata' = (…))`, which honors specializations | +| a generic table's `rowElementType` — a UML metaclass or a stereotype | `WhereType` on the v2 kind the metaclass or a standard stereotype [maps to](#mapping) (`Class` and «Block» → `PartDefinition`…), or `Named` over the usages a kind migrates to («Requirement», `Actor`, `UseCase`), which by type a query could not tell from a property; the abstract metaclasses list what they hold in UML, so `Type` and `Classifier` are every `Definition` plus the `ViewUsage`/`ViewpointUsage` a «View»/«Viewpoint» class became, `Namespace` adds `Package` and `StateUsage`, and `PackageableElement` adds `Package` and the dependencies — never the features a classifier owns; the `part` and `use case` usages actors and use cases became share their v2 type with properties, so these four, `Actor` and `UseCase` list them by name (`Named`) intersected with the scope; `Element` and `NamedElement` alone admit everything; a user stereotype the migration writes as a `metadata def` → `WhereMetadata('metadata' = (…))`, which honors specializations | | `columnIds` `QPROP:Element:name`, `documentation`, `qualifiedName`, `owner`, `Id`, `Text`, `classifier` | `Project(properties = (…))`, in column order — `Project` lists its properties ahead of its computed columns, so a table interleaving a tag column among these is reordered, with the note: `Id` reads `shortName` and `Text` `documentation`, since a «Requirement»'s `Id` and `Text` tags are written as the requirement def's short name and `doc` — a row that is no requirement has an empty cell under either, as it has in the tool, the element's identity being the `ID` column (`@id`); `classifier` reads `general`, the row's type by name; each column is headed by the tool's property name (`Id`, `Text`, `classifier`) through `Table.columnLabels`, the query property staying the column's name; `hideColumns` omits a column, the tool's own columns (`_NUMBER_`, `PROPERTY_COLUMN`, `VALUE_COLUMN`, `MARGIN_COLUMN`) are omitted silently, and other tool properties are omitted with the note | | `columnIds` `QPROP:stereotypeTags:<>.tag` — a stereotype tag | a standard «Requirement» tag as the property above; a user stereotype's tag as `Column(name = "", cell = { in row : ::; row. ?? "" })` over the feature of the `metadata def` the stereotype [became](#profiles-and-stereotypes), which reads what the row's applications bind it to — every value of a multi-valued tag, as one multi-valued cell, an enumeration literal by its name, as every element cell prints; a tag of a stereotype the migration does not write (library content), or one the archive does not define, is omitted with the reason; the same tag listed twice is two columns, the second named ` 2` but headed `` like the first, since column names are unique and headings need not be | | `columnWidth` (`-1` = automatic) | `Table.columnWidths`, one entry per projected column in `Project`'s order, `0` for automatic, a width staying with the column it was stated on when `Project` reorders; the renderers honour them proportionally ([Column widths](../manual/authoring.md#column-widths)); a width that is not a whole number is read as automatic, with the note | diff --git a/docs/reference/wire-contract.md b/docs/reference/wire-contract.md index 02d69739b3..46a64d7e84 100644 --- a/docs/reference/wire-contract.md +++ b/docs/reference/wire-contract.md @@ -1982,9 +1982,9 @@ $ … /Migrate -d '{"filePath":"Vehicle.xmi","toFormat":"sysml"}' "report": { "source": "Vehicle.xmi", "exporter": "Example UML Tool", - "summary": "migrated 93 element(s): 78 mapped, 12 approximated, 3 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to)", - "mapped": 78, - "approximated": 12, + "summary": "migrated 93 element(s): 77 mapped, 13 approximated, 3 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to)", + "mapped": 77, + "approximated": 13, "unmapped": 3, "skipped": 2 } diff --git a/internal/doc/queryexec/derivation_test.go b/internal/doc/queryexec/derivation_test.go index c09e9203d8..8feded6736 100644 --- a/internal/doc/queryexec/derivation_test.go +++ b/internal/doc/queryexec/derivation_test.go @@ -75,6 +75,13 @@ func TestExecuteRelatedDerivationDefinitions(t *testing.T) { assertRelated(t, fixture, "SegmentMassRequirement", "derivation", "incoming", 2, []string{"MirrorMassRequirement", "MassRequirement"}) + // A definition's ends subsetting requirement usages relate the usages. + assertRelated(t, fixture, "coolingCapacity", "derivation", "outgoing", 1, + []string{"fanSpeed"}) + assertRelated(t, fixture, "fanSpeed", "derivation", "incoming", 1, + []string{"coolingCapacity"}) + assertRelated(t, fixture, "fanSpeed", "derivation", "outgoing", 1, nil) + // A specializing definition keeps the ends it inherits, with their roles and // types, and an end redefined without a type keeps the redefined end's type; // an end typed by nothing stands for no requirement, not for an end's base type. diff --git a/internal/doc/queryexec/related_test.go b/internal/doc/queryexec/related_test.go index ae7f3e6c2b..186e808784 100644 --- a/internal/doc/queryexec/related_test.go +++ b/internal/doc/queryexec/related_test.go @@ -133,7 +133,7 @@ func TestExecuteRelatedConnectionsAllocationsAndAssertions(t *testing.T) { assertRelated(t, fixture, "telescope", "satisfaction", "outgoing", 1, []string{"massRequirement"}) assertRelated(t, fixture, "massRequirement", "satisfaction", "incoming", 1, - []string{"telescope"}) + []string{"telescope", "relayHub"}) // A satisfy without `by` relates the element stating it. assertRelated(t, fixture, "groundStation", "satisfaction", "outgoing", 1, []string{"pointingRequirement"}) @@ -151,8 +151,8 @@ func TestExecuteRelatedDeclaredRequirements(t *testing.T) { fixture := loadExecutionFixtureFile(t, "testdata/tmt_relationships.sysml") // A satisfy or verify assertion that declares its requirement relates the - // subject to the declared requirement usage itself and, when the - // declaration is typed, to its requirement definition. + // subject to the declared requirement usage itself and to the requirement + // definition typing it or the requirement usage it subsets. assertRelated(t, fixture, "scienceComputer", "satisfaction", "outgoing", 1, []string{"dataArchive::archiveRequirement", "DataRequirement"}) assertRelated(t, fixture, "dataArchive::archiveRequirement", "satisfaction", "incoming", 1, @@ -164,9 +164,16 @@ func TestExecuteRelatedDeclaredRequirements(t *testing.T) { assertRelated(t, fixture, "archiveVerification::archiveObjective::archiveCheck", "verification", "incoming", 1, []string{"scienceComputer"}) - // A declared requirement that subsets another is still the edge target. + // A declared requirement that subsets another is an edge target beside it. assertRelated(t, fixture, "relayHub", "satisfaction", "outgoing", 1, - []string{"relayControl::relayRequirement"}) + []string{"relayControl::relayRequirement", "massRequirement"}) + // Every subsetted requirement is, an alias followed to the requirement it names. + assertRelated(t, fixture, "backupHub", "satisfaction", "outgoing", 1, + []string{"backupControl::backupRequirement", "powerRequirement", "thermalRequirement"}) + assertRelated(t, fixture, "powerRequirement", "satisfaction", "incoming", 1, + []string{"backupHub"}) + assertRelated(t, fixture, "thermalRequirement", "satisfaction", "incoming", 1, + []string{"backupHub"}) assertRelated(t, fixture, "relayControl::relayRequirement", "satisfaction", "incoming", 1, []string{"relayHub"}) @@ -340,7 +347,7 @@ func TestExecuteRelatedComposesWithFiltersProjectionAndInvocation(t *testing.T) t.Fatalf("execute invoked: %v", err) } names = rowNames(invoked) - if len(names) != 1 || names[0] != "Observatory::telescope" { + if len(names) != 2 || names[0] != "Observatory::telescope" || names[1] != "Observatory::relayHub" { t.Fatalf("rows = %v", names) } diff --git a/internal/doc/queryexec/testdata/tmt_derivation.sysml b/internal/doc/queryexec/testdata/tmt_derivation.sysml index 0b6c807d62..9baa029bd9 100644 --- a/internal/doc/queryexec/testdata/tmt_derivation.sysml +++ b/internal/doc/queryexec/testdata/tmt_derivation.sysml @@ -85,6 +85,14 @@ package Observatory { end #RequirementDerivation::derive derivedRequirement : SegmentMassRequirement; } + // The migrator's form over requirement usages: ends subsetting the usages. + requirement coolingCapacity; + requirement fanSpeed; + connection def 'Derive Fan Speed' :> RequirementDerivation::Derivation { + end #RequirementDerivation::original originalRequirement :> coolingCapacity; + end #RequirementDerivation::derive derivedRequirement :> fanSpeed; + } + // Definitions specializing a derivation definition: one redefines the derived // end, one only the original, one nothing; an untyped end relates no requirement. requirement def AlignmentRequirement; diff --git a/internal/doc/queryexec/testdata/tmt_relationships.sysml b/internal/doc/queryexec/testdata/tmt_relationships.sysml index 82fb2b94f3..92389734f1 100644 --- a/internal/doc/queryexec/testdata/tmt_relationships.sysml +++ b/internal/doc/queryexec/testdata/tmt_relationships.sysml @@ -66,6 +66,13 @@ package Observatory { part relayControl { satisfy requirement relayRequirement :> massRequirement by relayHub; } + requirement powerRequirement; + requirement thermalRequirement; + alias Thermal for thermalRequirement; + part backupHub; + part backupControl { + satisfy requirement backupRequirement :> powerRequirement, Thermal by backupHub; + } verification archiveVerification : MassTest { objective archiveObjective { verify requirement archiveCheck by scienceComputer; diff --git a/internal/ir/view/interconnection.go b/internal/ir/view/interconnection.go index 3413bd47f3..ba643b93fb 100644 --- a/internal/ir/view/interconnection.go +++ b/internal/ir/view/interconnection.go @@ -31,7 +31,7 @@ func (r *Renderer) renderInterconnectionWithIDs(view *symbols.Symbol, exposed [] roots = append(roots, elem) } } - descendants := r.exposedDescendants(roots, r.interconnectionMembers) + descendants := r.exposedDescendants(roots, w.nestedFeatures) for _, elem := range exposed { switch { case r.drawsConnector(elem): @@ -126,6 +126,8 @@ func (w *featureWalk) featureNode(sym *symbols.Symbol, seen map[*symbols.Symbol] w.connectors = append(w.connectors, member) case casePin(sym, member): w.pin(node, member, pinDirection(member)) + case isReferencedIncludedCase(member): + // an `include u;` is drawn by the connection to the case it names case w.drawsFeature(member): child := w.featureNode(member, seen, depth+1, false) w.parent[child] = node @@ -235,12 +237,12 @@ func (r *Renderer) libraryDeclared(sym *symbols.Symbol) bool { return idx != nil && idx.Library(sym) } -// interconnectionMembers is the members featureNode draws as nested nodes: -// the feature-like of what an element declares, connectors being edges. -func (r *Renderer) interconnectionMembers(sym *symbols.Symbol) []*symbols.Symbol { +// nestedFeatures is the members featureNode draws as nested nodes of sym: +// the features this walk draws, connectors being edges. +func (w *featureWalk) nestedFeatures(sym *symbols.Symbol) []*symbols.Symbol { var out []*symbols.Symbol - for _, member := range r.containedMembers(sym) { - if featureLike(member) { + for _, member := range w.r.containedMembers(sym) { + if w.drawsFeature(member) && !isReferencedIncludedCase(member) { out = append(out, member) } } @@ -532,6 +534,8 @@ func featureLike(sym *symbols.Symbol) bool { symbols.SymbolEnumerationDef, symbols.SymbolEnumerationUsage, symbols.SymbolInterfaceDef, symbols.SymbolConnectionDef, symbols.SymbolAllocationDef, symbols.SymbolAnalysisCaseDef, symbols.SymbolAnalysisCaseUsage, + symbols.SymbolUseCaseDef, symbols.SymbolUseCaseUsage, + symbols.SymbolRequirementDef, symbols.SymbolRequirementUsage, symbols.SymbolKerMLType: return true } diff --git a/internal/ir/view/interconnection_includes_test.go b/internal/ir/view/interconnection_includes_test.go new file mode 100644 index 0000000000..220cb1942b --- /dev/null +++ b/internal/ir/view/interconnection_includes_test.go @@ -0,0 +1,23 @@ +package view + +import ( + "strings" + "testing" +) + +// An `include u;` in an exposed use case is a reference to another exposed +// case, drawn by the connection between them, not a node nested in the case. +func TestIncludedUseCaseIsNoNestedNode(t *testing.T) { + rendering := render(t, "interconnection-includes.sysml", "CaseViews::useCases") + if got := nodeNames(everyNode(rendering.Roots)); len(got) != 3 { + t.Errorf("nodes = %v, want the actor and the two cases only", got) + } + if len(rendering.Edges) != 2 { + t.Errorf("edges = %d, want the two connections: %+v", len(rendering.Edges), rendering.Edges) + } + for _, notice := range rendering.Notices { + if strings.Contains(notice, "without a position") || strings.Contains(notice, "does not expose") { + t.Errorf("notice %q, want none about the include", notice) + } + } +} diff --git a/internal/ir/view/testdata/interconnection-includes.sysml b/internal/ir/view/testdata/interconnection-includes.sysml new file mode 100644 index 0000000000..416700465c --- /dev/null +++ b/internal/ir/view/testdata/interconnection-includes.sysml @@ -0,0 +1,22 @@ +package Cases { + part Operator; + use case 'Control Instrument' { + include 'Manipulate Stage'; + } + use case 'Manipulate Stage'; + connection 'Control Instrument to Operator' connect 'Control Instrument' to Operator; + connection 'Control Instrument includes Manipulate Stage' connect 'Control Instrument' to 'Manipulate Stage'; +} + +package CaseViews { + private import Views::*; + + view useCases { + expose Cases::Operator; + expose Cases::'Control Instrument'; + expose Cases::'Manipulate Stage'; + expose Cases::'Control Instrument to Operator'; + expose Cases::'Control Instrument includes Manipulate Stage'; + render asInterconnectionDiagram; + } +} diff --git a/internal/semantic/semantics/relationship_edges.go b/internal/semantic/semantics/relationship_edges.go index 431ae2c505..eed6d5caf3 100644 --- a/internal/semantic/semantics/relationship_edges.go +++ b/internal/semantic/semantics/relationship_edges.go @@ -84,7 +84,10 @@ func (m *Model) satisfactionEdges(sym *symbols.Symbol, kind RelationshipKind) [] if !ok || usage.Kind != ast.UsageSatisfy || (usage.Keyword == "verify") != (kind == RelationshipVerification) { return nil } - var requirement, definition, subject *symbols.Symbol + var requirement, subject *symbols.Symbol + // originals are the requirements a declaring satisfy or verify is typed by + // or subsets, in declaration order. + var originals []*symbols.Symbol for _, rel := range usage.Relationships { if rel == nil || rel.Target == nil { continue @@ -93,14 +96,18 @@ func (m *Model) satisfactionEdges(sym *symbols.Symbol, kind RelationshipKind) [] if !ok || target == nil { continue } + if aliased, isAlias := m.resolver.ResolveAliasTarget(target); isAlias && aliased != nil { + target = aliased + } switch rel.Kind { - case ast.RelSubsets: - if !usage.DeclaresRequirement { - requirement = target - } - case ast.RelTyping: - if usage.DeclaresRequirement && target.Kind == symbols.SymbolRequirementDef { - definition = target + case ast.RelSubsets, ast.RelTyping: + switch { + case !usage.DeclaresRequirement: + if rel.Kind == ast.RelSubsets { + requirement = target + } + case isRequirementKind(target.Kind): + originals = append(originals, target) } case ast.RelSubject: subject = target @@ -119,12 +126,24 @@ func (m *Model) satisfactionEdges(sym *symbols.Symbol, kind RelationshipKind) [] return nil } out := []RelationshipEdge{{Source: subject, Target: requirement}} - if definition != nil { - out = append(out, RelationshipEdge{Source: subject, Target: definition}) + for _, original := range originals { + out = append(out, RelationshipEdge{Source: subject, Target: original}) } return out } +// isRequirementKind reports a requirement definition or usage, or one of the +// viewpoint and concern kinds specializing them. +func isRequirementKind(k symbols.SymbolKind) bool { + switch k { + case symbols.SymbolRequirementDef, symbols.SymbolViewpointDef, symbols.SymbolConcernDef, + symbols.SymbolRequirementUsage, symbols.SymbolSatisfyRequirementUsage, + symbols.SymbolViewpointUsage, symbols.SymbolConcernUsage: + return true + } + return false +} + func isObjectiveUsage(decl ast.Node) bool { usage, ok := decl.(*ast.Usage) return ok && usage.Kind == ast.UsageObjective @@ -209,10 +228,23 @@ func (m *Model) derivationUsageEnds(sym *symbols.Symbol) []derivationEnd { return ends } +// derivationDefinitionEnds reads a derivation def's ends: each refers to the +// requirements typing it, and to any requirement usage it subsets or references. func (m *Model) derivationDefinitionEnds(sym *symbols.Symbol) []derivationEnd { var ends []derivationEnd for _, feature := range m.EndFeatures(sym) { - ends = append(ends, derivationEnd{role: m.derivationRoleOf(feature), referents: m.requirementTypes(feature)}) + end := derivationEnd{role: m.derivationRoleOf(feature), referents: m.requirementTypes(feature)} + for _, rel := range relationshipsOfEnd(feature) { + if rel.Kind != ast.RelReferences && rel.Kind != ast.RelSubsets { + continue + } + target, ok := m.resolver.ResolveTarget(feature.OwnerScope, rel.Target) + if !ok || target == nil || m.isDerivationRoleFeature(target) || !m.conformsToLibrary(target, "Requirements::RequirementCheck") { + continue + } + end.referents = append(end.referents, target) + } + ends = append(ends, end) } return ends } diff --git a/internal/semantic/semantics/relationship_edges_test.go b/internal/semantic/semantics/relationship_edges_test.go index be0a6f2e7a..e27accda82 100644 --- a/internal/semantic/semantics/relationship_edges_test.go +++ b/internal/semantic/semantics/relationship_edges_test.go @@ -68,6 +68,10 @@ func TestRelationshipEdgesOf(t *testing.T) { allocation alloc allocate source to target; requirement req; part satisfiedBy { satisfy req; } + requirement primary; + requirement secondary; + alias Secondary for secondary; + part pump { satisfy requirement proof :> primary, Secondary; } verification def Check; verification verified : Check { objective { verify req; } @@ -91,6 +95,9 @@ func TestRelationshipEdgesOf(t *testing.T) { {"P::alloc", "", RelationshipAllocation, [][2]string{{"P::source", "P::target"}}}, {"P::satisfiedBy", "satisfy", RelationshipSatisfaction, [][2]string{{"P::satisfiedBy", "P::req"}}}, {"P::verified", "verify", RelationshipVerification, [][2]string{{"P::verified", "P::req"}}}, + // A declaring satisfy relates its subject to the declared requirement and + // to every requirement it subsets, an alias followed to its target. + {"P::pump", "satisfy", RelationshipSatisfaction, [][2]string{{"P::pump", "P::pump::proof"}, {"P::pump", "P::primary"}, {"P::pump", "P::secondary"}}}, } for _, tc := range cases { t.Run(tc.symbol+"/"+string(tc.kind), func(t *testing.T) { diff --git a/internal/translate/migrate/classify.go b/internal/translate/migrate/classify.go index 852de58b1f..ab2dfba955 100644 --- a/internal/translate/migrate/classify.go +++ b/internal/translate/migrate/classify.go @@ -20,7 +20,9 @@ const ( catAttributeDef catEnumDef catConstraintDef - catRequirementDef + // catRequirement is a v1 requirement, written as a requirement usage, as a + // tool's users draw one: a satisfy, verify or refine joins it directly. + catRequirement catConnectionDef catIndividualDef catVerificationDef @@ -31,8 +33,11 @@ const ( // catCalcDef is an opaque or function behavior computing a result. catCalcDef catStateDef - // catUseCaseDef is a UML use case, whatever incidental stereotype it carries. - catUseCaseDef + // catUseCase is a UML use case, whatever incidental stereotype it carries, + // written as a use case usage: v2 draws connections between usages, not definitions. + catUseCase + // catActor is a UML actor, written as a part usage a use case's connections join. + catActor // catView is a v1 «View», written as a view usage typed by its viewpoints. catView // catViewpoint is a v1 «Viewpoint», written as a view definition. @@ -69,8 +74,8 @@ func (c category) keyword() string { return "enum def" case catConstraintDef: return "constraint def" - case catRequirementDef: - return "requirement def" + case catRequirement: + return "requirement" case catConnectionDef: return "connection def" case catIndividualDef: @@ -85,8 +90,10 @@ func (c category) keyword() string { return "calc def" case catStateDef: return "state def" - case catUseCaseDef: - return "use case def" + case catUseCase: + return "use case" + case catActor: + return "part" case catView: return "view" case catViewpoint: @@ -118,8 +125,8 @@ func (c category) metaclass() string { return "SysML::EnumerationDefinition" case catConstraintDef: return "SysML::ConstraintDefinition" - case catRequirementDef: - return "SysML::RequirementDefinition" + case catRequirement: + return "SysML::RequirementUsage" case catConnectionDef: return "SysML::ConnectionDefinition" case catVerificationDef: @@ -132,8 +139,10 @@ func (c category) metaclass() string { return "SysML::CalculationDefinition" case catStateDef: return "SysML::StateDefinition" - case catUseCaseDef: - return "SysML::UseCaseDefinition" + case catUseCase: + return "SysML::UseCaseUsage" + case catActor: + return "SysML::PartUsage" case catMetadataDef: return "SysML::MetadataDefinition" } @@ -498,7 +507,7 @@ func (m *migration) classify(e *sysmlv1.Element) (category, string) { case "Class", "Component": return classifyClass(e) case "Actor": - return catPartDef, "a UML actor is written as a part def" + return catActor, "a UML actor is written as a part usage" case "AssociationClass": return catConnectionDef, "" case "Association": @@ -528,7 +537,7 @@ func (m *migration) classify(e *sysmlv1.Element) (category, string) { case "Reception": return catUnmapped, "a reception names the signal its owner accepts, which the owner's behaviors carry as accept" case "UseCase": - return catUseCaseDef, "" + return catUseCase, "" case "Collaboration", "Node", "Device", "ExecutionEnvironment", "Artifact": return catUnmapped, "no v2 form for a UML " + e.Type case "DurationObservation", "TimeObservation": @@ -544,7 +553,7 @@ func classifyClass(e *sysmlv1.Element) (category, string) { case simulationConfig(e) != nil: return catSimConfig, "" case has(e, requirementStereotypes...): - return catRequirementDef, "" + return catRequirement, "" case has(e, "ConstraintBlock"): return catConstraintDef, "" case has(e, "InterfaceBlock"): @@ -572,7 +581,7 @@ func (m *migration) classifyInstance(e *sysmlv1.Element) (category, string) { } if association := m.instanceAssociation(e); association != nil && !m.associationAsConnectionDef(association) { if m.actors[association] != nil { - return catUnmapped, "the link's association is written as an actor usage, so there is no connection def to specialize" + return catUnmapped, "the link's association is written as a connection between the actor and the use case, so there is no connection def to specialize" } return catUnmapped, "the link's association is written as its member-end properties, so there is no connection def to specialize" } @@ -664,7 +673,7 @@ func (m *migration) instanceClassifiers(e *sysmlv1.Element) (occurrences, values notes = append(notes, classifierSubject+qualifiedName(c)+" is not migrated") case cc == catAttributeDef, cc == catEnumDef: values = append(values, c) - case cc == catView: + case cc == catView, cc == catActor, cc == catUseCase, cc == catRequirement: notes = append(notes, classifierSubject+qualifiedName(c)+" is written as a "+cc.keyword()+" usage, which an individual cannot specialize") default: occurrences = append(occurrences, c) diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 720e7d5a2b..610f9f6cbb 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -426,6 +426,9 @@ func (m *migration) drawsAsEdge(f viewForm, el *sysmlv1.Element) bool { // places reports whether a view of form f exposing x shows el, which ref names, as a // node a Layout positions: an element it exposes or a node of its subject's graph. func (m *migration) places(x exposures, f viewForm, el *sysmlv1.Element, ref string) bool { + if f.useCases && m.connOf(el) != nil { + return false + } return !m.drawsAsEdge(f, el) && (x.exposed(ref) || inGraph(f, el) && m.drawsNode(el, f)) } @@ -649,7 +652,7 @@ func (m *migration) exposures(d *sysmlv1.Diagram, host *sysmlv1.Element, form vi x.dangling++ continue } - ref := m.exposure(shown.Element, host) + ref := m.shownRef(shown.Element, host, form) switch { case m.graphDraws(form, shown.Element): x.drawn++ @@ -659,6 +662,13 @@ func (m *migration) exposures(d *sysmlv1.Diagram, host *sysmlv1.Element, form vi x.unwritten++ default: add(ref) + if c := m.connOf(shown.Element); form.useCases && c != nil { + // The connection stands for the edge; its members would shadow the usages. + for _, also := range m.connRefs(c.of, host) { + add(also) + } + continue + } for _, also := range m.edgeRefs(shown.Element, host) { add(also) } @@ -703,7 +713,10 @@ func (m *migration) exposure(e, scope *sysmlv1.Element) string { return scalarValuesPrefix + sv } if link := m.actorLinkOf(e); link != nil { - return m.memberRef(link.useCase, scope) + "::" + writeName(link.name) + if link.conn == nil { + return "" + } + return m.hostedRef(link.conn.host, link.conn.name, scope) } if ref := m.monteCarloExposure(e, scope); ref != "" { return ref @@ -717,8 +730,8 @@ func (m *migration) exposure(e, scope *sysmlv1.Element) string { return "" } -// actorLinkOf is the actor usage that stands for e: an anonymous association to -// an actor, or its end at the actor; nil when e is written by itself. +// actorLinkOf is the link whose connection stands for e: an anonymous association +// to an actor, or its end at the actor; nil when e is written by itself. func (m *migration) actorLinkOf(e *sysmlv1.Element) *actorLink { if e == nil { return nil @@ -734,6 +747,21 @@ func (m *migration) actorLinkOf(e *sysmlv1.Element) *actorLink { return link } +// shownRef is what a view of form f exposes for e, from scope: on a use case +// diagram the usage written for a definition, or for an association the +// connection alone (see connRefs); otherwise what exposure names. +func (m *migration) shownRef(e, scope *sysmlv1.Element, f viewForm) string { + if f.useCases { + if u := m.usageIn(e); u != nil { + return m.usageRef(u, scope) + } + if c := m.connOf(e); c != nil { + return m.connRefs(c.of, scope)[0] + } + } + return m.exposure(e, scope) +} + // exposable is the declaration written for e that a view can expose: its own, the // operation a behavior is the method of, or a state's named inline action; nil for none. func (m *migration) exposable(e *sysmlv1.Element) *sysmlv1.Element { @@ -887,7 +915,7 @@ func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeomet geo.lines = append(geo.lines, routes...) geo.lines = append(geo.lines, dress.lines...) if len(rec.Placements)+len(rec.Connectors) > 0 { - clauses := append([]string{layoutClause(p.written, p.unexposed, p.dangling, len(rec.Placements)), routeClause(p.reasons, len(rec.Connectors))}, dress.notes...) + clauses := append([]string{layoutClause(p.written, p.unexposed, p.dangling, p.onEdges, len(rec.Placements)), routeClause(p.reasons, len(rec.Connectors))}, dress.notes...) geo.note = fmt.Sprintf("laid out from %s: %s", source, strings.Join(clauses, ", ")) } else if len(dress.notes) > 0 { geo.note = fmt.Sprintf("dressed from %s: %s", streamSource, strings.Join(dress.notes, ", ")) @@ -908,6 +936,7 @@ type layoutPlan struct { refOf map[string]string written int unexposed int + onEdges int dangling int reasons map[string]int } @@ -935,9 +964,14 @@ func (p *layoutPlan) placements(rec *mtip.Diagram) []string { p.dangling++ continue } + if c := m.connOf(el); p.form.useCases && c != nil && c.of != el { + // An association end's label lies on the connection's route. + p.onEdges++ + continue + } ref := "" if vertexBase(el) == "" || m.vertexWritten(el) { - ref = m.exposure(el, v.host) + ref = m.shownRef(el, v.host, p.form) } if ref == "" || !m.places(p.x, p.form, el, ref) { s.PlacementsUnexposed++ @@ -1029,7 +1063,7 @@ func (m *migration) drawnRef(x exposures, f viewForm, host *sysmlv1.Element, id if el == nil { return "" } - if ref := m.exposure(el, host); ref != "" && m.places(x, f, el, ref) { + if ref := m.shownRef(el, host, f); ref != "" && m.places(x, f, el, ref) { return ref } refs, why := m.routeTarget(el, host, f) @@ -1041,12 +1075,15 @@ func (m *migration) drawnRef(x exposures, f viewForm, host *sysmlv1.Element, id // layoutClause words the placements of the layout note: how many of the // record's shown elements were positioned, and why the rest were not. -func layoutClause(written, unexposed, dangling, total int) string { +func layoutClause(written, unexposed, dangling, onEdges, total int) string { clause := fmt.Sprintf("%d of %d shown elements positioned", written, total) var parts []string if unexposed > 0 { parts = append(parts, fmt.Sprintf("%d not exposed", unexposed)) } + if onEdges > 0 { + parts = append(parts, fmt.Sprintf("%d lying on connections", onEdges)) + } if dangling > 0 { parts = append(parts, fmt.Sprintf("%d resolving to no element", dangling)) } diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go index b93cd7e526..17efb0a440 100644 --- a/internal/translate/migrate/diagrams_internal_test.go +++ b/internal/translate/migrate/diagrams_internal_test.go @@ -480,14 +480,15 @@ func TestExposeOfUnhostedDiagramFailsPerClient(t *testing.T) { func TestLayoutClauseWording(t *testing.T) { for _, tc := range []struct { - written, unexposed, dangling, total int - want string + written, unexposed, dangling, onEdges, total int + want string }{ - {1, 0, 0, 1, "1 of 1 shown elements positioned"}, - {1, 1, 1, 3, "1 of 3 shown elements positioned (1 not exposed, 1 resolving to no element)"}, - {0, 2, 0, 2, "0 of 2 shown elements positioned (2 not exposed)"}, + {1, 0, 0, 0, 1, "1 of 1 shown elements positioned"}, + {1, 1, 1, 0, 3, "1 of 3 shown elements positioned (1 not exposed, 1 resolving to no element)"}, + {0, 2, 0, 0, 2, "0 of 2 shown elements positioned (2 not exposed)"}, + {2, 0, 0, 2, 4, "2 of 4 shown elements positioned (2 lying on connections)"}, } { - got := layoutClause(tc.written, tc.unexposed, tc.dangling, tc.total) + got := layoutClause(tc.written, tc.unexposed, tc.dangling, tc.onEdges, tc.total) if got != tc.want { t.Errorf("layoutClause = %q, want %q", got, tc.want) } diff --git a/internal/translate/migrate/edges.go b/internal/translate/migrate/edges.go index eae5d96bb2..c3dfc161e2 100644 --- a/internal/translate/migrate/edges.go +++ b/internal/translate/migrate/edges.go @@ -364,6 +364,9 @@ func routeKindName(exported string, el *sysmlv1.Element) string { // routeTarget is every member a route of el is pinned to, as scope's view of // form f exposes them; or nil and the reason there is none. func (m *migration) routeTarget(el, scope *sysmlv1.Element, f viewForm) ([]string, string) { + if c := m.connOf(el); f.useCases && c != nil { + return m.connRefs(c.of, scope)[:1], "" + } if nameableEdge(el) { em, ok := m.edgeMembers[el] switch { @@ -392,6 +395,9 @@ type viewForm struct { rendering string definition string subject *sysmlv1.Element + // useCases marks a use case diagram, drawn as the usages and connections + // written for its actors and use cases rather than their definitions. + useCases bool } // graphDefinitions pairs the words naming a UML behavior diagram family with the @@ -420,6 +426,9 @@ func (m *migration) form(d *sysmlv1.Diagram) (viewForm, string) { return f, "" } kind := strings.ToLower(d.Kind + " / " + d.UMLKind) + if strings.Contains(kind, "use case") { + return viewForm{rendering: interconnectionRendering, useCases: true}, "" + } for _, g := range graphDefinitions { for _, w := range g.words { if !strings.Contains(kind, w) { diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 6824338ed3..8884451eaf 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -151,6 +151,7 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result { concerns: map[*sysmlv1.Element]*concernInfo{}, concernLists: map[*sysmlv1.Element][]*sysmlv1.Element{}, taken: map[*sysmlv1.Element]map[string]bool{}, + takenBy: map[*sysmlv1.Element]map[string]*sysmlv1.Element{}, parallel: map[*sysmlv1.Element]string{}, exposed: map[*sysmlv1.Element]string{}, methodOf: map[*sysmlv1.Element]*sysmlv1.Element{}, @@ -211,6 +212,9 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result { pictureOf: map[*sysmlv1.Diagram]*pictures{}, buried: map[*sysmlv1.Element]bool{}, actors: map[*sysmlv1.Element]*actorLink{}, + defUsages: map[*sysmlv1.Element]map[*sysmlv1.Element]*defUsage{}, + conns: map[*sysmlv1.Element]*defConn{}, + moreConns: map[*sysmlv1.Element][]*defConn{}, monteCarlo: map[*sysmlv1.Element]*monteCarloCase{}, strict: opts.Strict, layout: opts.Layout, @@ -366,6 +370,8 @@ type migration struct { unplaced map[*sysmlv1.Element]*placement // taken holds synthesized names reserved in a body, by owner. taken map[*sysmlv1.Element]map[string]bool + // takenBy is the element each taken name was reserved for; nil for none. + takenBy map[*sysmlv1.Element]map[string]*sysmlv1.Element // opened holds the member names of each synthesized declaration being // written, outermost first; a reference written inside them avoids those names. opened []columnNames @@ -528,6 +534,11 @@ type migration struct { // actors gives each association linking a use case to an actor the actor // usage it is written as in the use case's body. actors map[*sysmlv1.Element]*actorLink + // defUsages are the usages written in a package, by definition, for the + // connections joining them; conns the connection written for an association or «Refine». + defUsages map[*sysmlv1.Element]map[*sysmlv1.Element]*defUsage + conns map[*sysmlv1.Element]*defConn + moreConns map[*sysmlv1.Element][]*defConn // framed marks the comments a viewpoint's concernList names, written as its concerns. framed map[*sysmlv1.Element]bool // concerns records comments named by viewpoint and stakeholder concern lists. @@ -645,6 +656,7 @@ func (m *migration) prepare() { m.prepareConcerns() var reachers, configs, laned, associations, behaviors []*sysmlv1.Element var links []*actorLink + var joined []*sysmlv1.Element var walk func(e *sysmlv1.Element) walk = func(e *sysmlv1.Element) { m.distinguish(e) @@ -709,6 +721,9 @@ func (m *migration) prepare() { m.nameFor(r) } } + if has(e, "Refine") { + joined = append(joined, e) + } if has(e, "Allocate") { for _, c := range m.model.Refs(e, "client") { m.allocated[c] = append(m.allocated[c], m.model.Refs(e, "supplier")...) @@ -757,6 +772,8 @@ func (m *migration) prepare() { links = append(links, link) } } + case "Include", "Extend": + joined = append(joined, e) } for _, c := range e.Children { walk(c) @@ -769,6 +786,7 @@ func (m *migration) prepare() { } m.indexSnapshots(configs) m.placeActors(links) + m.planConnections(joined) for _, e := range reachers { m.exposeReached(e) } @@ -1145,7 +1163,7 @@ func (m *migration) member(e *sysmlv1.Element) { m.extend(e) return case "ExtensionPoint": - m.unmapped(e, "v2 has no extension points; an extending use case is written as a dependency on the extended one") + m.unmapped(e, "v2 has no extension points; an extending use case is written as a connection to the extended one") return case "Comment": // A comment in a non-ownedComment role is still a comment. @@ -1346,7 +1364,7 @@ func (m *migration) classifierHeader(e *sysmlv1.Element, cat category, name stri b.WriteString(cat.keyword()) } b.WriteByte(' ') - if cat == catRequirementDef { + if cat == catRequirement { if id := m.requirementID(e); id != "" { b.WriteString("<" + writeName(id) + "> ") } @@ -1391,10 +1409,10 @@ func (m *migration) classifierBody(e *sysmlv1.Element, cat category, header stri case catConstraintDef: m.w.block(header, func() { m.constraintBody(e) }) return - case catRequirementDef: + case catRequirement: m.w.block(header, func() { m.requirementBody(e) }) return - case catUseCaseDef: + case catUseCase: m.w.block(header, func() { m.useCaseBody(e) }) return case catView: @@ -2088,7 +2106,11 @@ func (m *migration) association(e *sysmlv1.Element) { if e.Name == "" { missing := m.dangling(e, "memberEnd") if link != nil { - m.add(e, Mapped, m.actorTarget(link), "the anonymous association to the actor is written as an actor of the use case") + if link.conn == nil { + m.add(e, Unmapped, "", joinNotes("the anonymous association to the actor is in no package a connection between the actor and the use case can be written in", missing)) + return + } + m.add(e, Mapped, link.conn.target(m), joinNotes("the anonymous association to the actor is written as a connection between the actor and the use case", missing)) return } if e.Type == "Association" && !m.associationAsConnectionDef(e) { @@ -2098,8 +2120,8 @@ func (m *migration) association(e *sysmlv1.Element) { name = m.nameFor(e) m.add(e, Approximated, m.v2Name(e), joinNotes("the anonymous "+e.Type+" owns every end, so it is written as connection def "+name, missing)) } - if link != nil { - m.add(e, Mapped, m.v2Name(e), "the association is also written as the actor "+link.name+" of the use case "+m.v2Name(link.useCase)) + if link != nil && link.conn != nil { + m.add(e, Mapped, m.v2Name(e), "the association's one usage is the connection "+link.conn.name+" joining the actor and the use case "+m.v2Name(link.useCase)) } header := "connection def " + writeName(name) if gens, _ := m.generals(e, catConnectionDef); gens != "" { @@ -2211,7 +2233,7 @@ func (m *migration) featureKeyword(p *sysmlv1.Element, owner category) (keyword, if t == nil { return "ref", "", "the untyped property is written as a reference usage" } - if owner == catUseCaseDef && t.Type == "Actor" && m.written(t) { + if owner == catUseCase && t.Type == "Actor" && m.written(t) { return "actor", "", "" } kw, note := m.typeKeyword(t) @@ -2275,7 +2297,7 @@ func (m *migration) typeKeyword(t *sysmlv1.Element) (keyword, note string) { return "port", "" } return "port", "a property typed by an interface block is written as a port" - case catRequirementDef: + case catRequirement: return "requirement", "" case catActionDef: return "action", "" @@ -2283,8 +2305,10 @@ func (m *migration) typeKeyword(t *sysmlv1.Element) (keyword, note string) { return "state", "" case catCalcDef: return "calc", "" - case catUseCaseDef: + case catUseCase: return "use case", "" + case catActor: + return "part", "" case catView: return "view", "" case catViewpoint: @@ -2548,7 +2572,7 @@ func (m *migration) featureTyping(b *strings.Builder, p, ind *sysmlv1.Element, p // when the type becomes a usage, as a view does, else typing. func (m *migration) typing(t *sysmlv1.Element) string { if t != nil { - if cat, _ := m.classify(t); cat == catView { + if cat, _ := m.classify(t); cat == catView || cat == catUseCase || cat == catActor || cat == catRequirement { return " :> " } } @@ -3477,9 +3501,11 @@ func (m *migration) dependency(d *sysmlv1.Element) { placed := len(pl.targets) for i, p := range pairs { name := m.nameOf(d) - if name != "" && i+placed > 0 { - name = m.freshName(m.scope, name) - pl.notes = append(pl.notes, fmt.Sprintf("pair %d is named %s so the pairs stay distinct", i+placed+1, name)) + if name != "" { + name = m.reserveNameFor(d, m.scope, name) + if i+placed > 0 { + pl.notes = append(pl.notes, fmt.Sprintf("pair %d is named %s so the pairs stay distinct", i+placed+1, name)) + } } target, written, note := m.dependencyPair(d, pl, name, p.client, p.supplier) if written { @@ -3504,8 +3530,13 @@ func (m *migration) dependency(d *sysmlv1.Element) { // freshName returns name, or name with a numeric suffix, not yet taken in owner, // and reserves it. func (m *migration) freshName(owner *sysmlv1.Element, name string) string { + return m.freshNameBut(nil, owner, name) +} + +// freshNameBut is freshName for the declaration written for e, which may keep e's own name. +func (m *migration) freshNameBut(e, owner *sysmlv1.Element, name string) string { base := name - for i := 2; m.nameTaken(owner, name); i++ { + for i := 2; m.nameTakenBut(e, owner, name); i++ { name = fmt.Sprintf("%s %d", base, i) } m.take(owner, name) @@ -3592,6 +3623,19 @@ func (m *migration) dependencyPair(d *sysmlv1.Element, pl *placement, name strin if name != "" { target = m.qualified(append(m.segments(m.scope), name)) } + if has(d, "Refine") { + conn := m.pairConn(d, client, supplier) + if conn == nil { + conn = m.kindConnection(d, name, client, supplier, "refines") + } + if conn != nil { + m.wroteEdgeAlso(d, conn.host, "connection", nil, conn.name) + if len(nodes) > 0 { + pl.nodePairs = append(pl.nodePairs, nodePair{nodes, conn.target(m)}) + } + return conn.target(m), true, refineConnectionNote + } + } kw := "dependency" if has(d, "Allocate") { kw = "allocation" @@ -3648,7 +3692,7 @@ func (m *migration) dependencyPair(d *sysmlv1.Element, pl *placement, name strin // of the block owning the satisfying property, returning the v2 name written, // a note, and whether it was written. func (m *migration) satisfy(d, client, req *sysmlv1.Element, name string) (string, string, bool) { - if rc, _ := m.classify(req); rc != catRequirementDef { + if rc, _ := m.classify(req); rc != catRequirement { return "", "the supplier " + qualifiedName(req) + " is not a requirement", false } scope, by := m.usageContext(client) @@ -3663,12 +3707,12 @@ func (m *migration) satisfy(d, client, req *sysmlv1.Element, name string) (strin } m.extras[scope] = append(m.extras[scope], func() { m.wroteEdgeAlso(d, scope, "satisfy", nil, name) - decl := "satisfy requirement " + decl := "satisfy " if name != "" { - decl += writeName(name) + " " + decl += "requirement " + writeName(name) + " :> " m.madeUp(d, writeName(name)) } - decl += ": " + m.ref(req, scope) + decl += m.ref(req, scope) if by != "" { decl += " by " + by } @@ -3707,7 +3751,7 @@ func (m *migration) usageContext(client *sysmlv1.Element) (*sysmlv1.Element, str // verify places `verify requirement` in the objective of the test case. func (m *migration) verify(d, client, req *sysmlv1.Element, name string) (string, string, bool) { - if rc, _ := m.classify(req); rc != catRequirementDef { + if rc, _ := m.classify(req); rc != catRequirement { return "", "the supplier " + qualifiedName(req) + " is not a requirement", false } if cc, _ := m.classify(client); cc != catVerificationDef { @@ -3728,12 +3772,12 @@ func (m *migration) verify(d, client, req *sysmlv1.Element, name string) (string } m.extras[client] = append(m.extras[client], func() { m.wroteEdgeAlso(d, client, "verify", nest, name) - decl := "verify requirement " + decl := "verify " if name != "" { - decl += writeName(name) + " " + decl += "requirement " + writeName(name) + " :> " m.madeUp(d, writeName(name)) } - m.w.block(decl+": "+m.ref(req, client), func() { m.metadataUsages(d) }) + m.w.block(decl+m.ref(req, client), func() { m.metadataUsages(d) }) }) if name == "" { return m.v2Name(client), note, true @@ -3744,7 +3788,7 @@ func (m *migration) verify(d, client, req *sysmlv1.Element, name string) (string // definitionEnd reports whether e is written as a v2 definition, which an // allocation usage cannot take as an end: only a feature is a usage end. func (m *migration) definitionEnd(e *sysmlv1.Element) bool { - if m.asUsage[e] { + if m.asUsage[e] || m.selfUsage(e) { return false } switch c, _ := m.classify(e); c { @@ -3846,7 +3890,7 @@ func (m *migration) allocationDef(d *sysmlv1.Element, name string, client, suppl func (m *migration) derive(d *sysmlv1.Element, name string, derived, original *sysmlv1.Element) (string, string) { dc, _ := m.classify(derived) oc, _ := m.classify(original) - if dc != catRequirementDef || oc != catRequirementDef { + if dc != catRequirement || oc != catRequirement { return "", "both ends of a derive must be requirements" } if name == "" { @@ -3858,8 +3902,8 @@ func (m *migration) derive(d *sysmlv1.Element, name string, derived, original *s m.names[d] = name } m.w.block("connection def "+writeName(name)+" :> RequirementDerivation::Derivation", func() { - m.w.line("end #RequirementDerivation::original originalRequirement : " + m.ref(original, d) + ";") - m.w.line("end #RequirementDerivation::derive derivedRequirement : " + m.ref(derived, d) + ";") + m.w.line("end #RequirementDerivation::original originalRequirement" + m.typing(original) + m.ref(original, d) + ";") + m.w.line("end #RequirementDerivation::derive derivedRequirement" + m.typing(derived) + m.ref(derived, d) + ";") m.metadataUsages(d) }) segs := append(m.segments(m.scope), name) @@ -3965,7 +4009,7 @@ func (m *migration) documentation(e *sysmlv1.Element) string { if vertexBase(e) != "" || e.Role == "node" || e.Type == "Region" && e.Role == "region" { return "" } - if cat, _ := m.classify(e); cat == catRequirementDef { + if cat, _ := m.classify(e); cat == catRequirement { if text := requirementText(e); text != "" { return m.proseText(text, e) } diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 0127cd7d9f..f94d32bbe9 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -115,30 +115,35 @@ func (m *migration) writtenName(e *sysmlv1.Element) string { } func (m *migration) nameTaken(owner *sysmlv1.Element, name string) bool { + return m.nameTakenBut(nil, owner, name) +} + +// nameTakenBut is nameTaken disregarding e, whose own name the declaration +// written for it is free to keep. +func (m *migration) nameTakenBut(e, owner *sysmlv1.Element, name string) bool { if m.taken[owner][name] { return true } if owner == nil { - return m.topLevelNamed(name) + return m.topLevelNamedBut(e, name) } for _, c := range owner.Children { - if m.nameOf(c) == name { + if c != e && m.nameOf(c) == name { return true } } return false } -// topLevelNamed reports whether a declaration at the document's top level is -// named name: a member of the root model, or a root written as a declaration. -func (m *migration) topLevelNamed(name string) bool { +// topLevelNamedBut is topLevelNamed disregarding e (see nameTakenBut). +func (m *migration) topLevelNamedBut(e *sysmlv1.Element, name string) bool { for _, r := range m.model.Roots { switch { case m.flattened(r): - if m.nameTaken(r, name) { + if m.nameTakenBut(e, r, name) { return true } - case r.Type != "Model" && m.nameOf(r) == name: + case r != e && r.Type != "Model" && m.nameOf(r) == name: return true } } @@ -147,10 +152,56 @@ func (m *migration) topLevelNamed(name string) bool { // take reserves a synthesized member name in owner's body. func (m *migration) take(owner *sysmlv1.Element, name string) { + m.takeFor(nil, owner, name) +} + +// takeFor reserves name in owner for the declaration written for e, which +// claimName hands it to once. +func (m *migration) takeFor(e, owner *sysmlv1.Element, name string) { if m.taken[owner] == nil { m.taken[owner] = map[string]bool{} + m.takenBy[owner] = map[string]*sysmlv1.Element{} } m.taken[owner][name] = true + m.takenBy[owner][name] = e +} + +// reserveNameFor is freshNameBut reserving the name for e to claim (see claimName). +func (m *migration) reserveNameFor(e, owner *sysmlv1.Element, name string) string { + name = m.freshNameBut(e, owner, name) + m.takenBy[owner][name] = e + return name +} + +// claimName is the name a declaration written for e takes in owner: the one +// reserved for it, consumed, or else name made fresh. +func (m *migration) claimName(e, owner *sysmlv1.Element, name string) string { + if in, ok := m.reservedIn(e, owner, name); ok { + m.takenBy[in][name] = nil + return name + } + return m.freshNameBut(e, owner, name) +} + +// reservedIn is the owner, among those nameTakenBut consults for owner, in +// which name is reserved for e; the top level and a flattened root are one. +func (m *migration) reservedIn(e, owner *sysmlv1.Element, name string) (*sysmlv1.Element, bool) { + if e == nil { + return nil, false + } + if m.takenBy[owner][name] == e { + return owner, true + } + if owner == nil { + for _, r := range m.model.Roots { + if m.flattened(r) && m.takenBy[r][name] == e { + return r, true + } + } + } else if m.flattened(owner) && m.takenBy[nil][name] == e { + return nil, true + } + return nil, false } // identifierSuffix turns a name into the tail of a compound identifier: its @@ -303,7 +354,7 @@ func (m *migration) isUsage(e *sysmlv1.Element) bool { return true } cat, _ := m.classify(e) - return cat == catView + return cat == catView || cat == catUseCase || cat == catActor || cat == catRequirement } // scopeChain lists scope and its ancestors, innermost first, stopping at the diff --git a/internal/translate/migrate/ocllower.go b/internal/translate/migrate/ocllower.go index 773dec7d18..83bea57e87 100644 --- a/internal/translate/migrate/ocllower.go +++ b/internal/translate/migrate/ocllower.go @@ -395,16 +395,13 @@ func (l *oclLowering) hasStereotype(n *oclNode, v oclValue) (oclValue, error) { } l.need(host.variable, "Property") return oclValue{text: host.text + ".direction->" + l.function("notEmpty") + "()", kind: "Boolean", single: true}, nil - case stereotypeTypes[v.tag].types != nil: + case stereotypeTypes[v.tag].types != nil || stereotypeTypes[v.tag].usages != nil: t := stereotypeTypes[v.tag] if t.note != "" { l.notes = append(l.notes, t.note) } - types := make([]string, len(t.types)) - for i, typ := range t.types { - types[i] = stringLiteral(typ) - } - return oclValue{text: l.prefix + "WhereType(source = " + host.text + ", type = (" + strings.Join(types, ", ") + "))->" + l.function("notEmpty") + "()", kind: "Boolean", single: true}, nil + f := l.m.nameUsages(typeFilter{types: t.types, usageKinds: t.usages}) + return oclValue{text: f.query(qlit(host.text)).text(l.prefix) + "->" + l.function("notEmpty") + "()", kind: "Boolean", single: true}, nil } if def := l.m.stereotypeNamed(v.tag); def != nil && l.m.written(def) { return oclValue{text: l.prefix + "WhereMetadata(source = " + host.text + ", 'metadata' = (" + stringLiteral(l.m.plainName(def)) + "))->" + l.function("notEmpty") + "()", kind: "Boolean", single: true}, nil diff --git a/internal/translate/migrate/query.go b/internal/translate/migrate/query.go index ffdeef59f4..72e9066f5a 100644 --- a/internal/translate/migrate/query.go +++ b/internal/translate/migrate/query.go @@ -38,6 +38,10 @@ func qshared(src qx) qx { } return qx{shared: &src} } + +// qempty is the empty sequence, a row set with no rows that walks no scope. +func qempty() qx { return qlit("()") } + func qarg1(name string, v qx) qarg { return qarg{name: name, val: v} } func qlist(name string, vs ...qx) qarg { return qarg{name: name, list: vs, many: true} } diff --git a/internal/translate/migrate/realization.go b/internal/translate/migrate/realization.go index dea800f5c2..cd5341d5a5 100644 --- a/internal/translate/migrate/realization.go +++ b/internal/translate/migrate/realization.go @@ -79,7 +79,7 @@ func (m *migration) portTypedBy(owner, t *sysmlv1.Element) *sysmlv1.Element { // another of the same category with `:>`. func specializable(cat category) bool { switch cat { - case catPartDef, catOccurrenceDef, catPortDef, catAttributeDef, catConstraintDef, catRequirementDef, catConnectionDef, + case catPartDef, catOccurrenceDef, catPortDef, catAttributeDef, catConstraintDef, catRequirement, catConnectionDef, catVerificationDef, catItemDef, catActionDef, catCalcDef, catStateDef: return true } diff --git a/internal/translate/migrate/synthesized_names_internal_test.go b/internal/translate/migrate/synthesized_names_internal_test.go index d34d143646..6ba0d06036 100644 --- a/internal/translate/migrate/synthesized_names_internal_test.go +++ b/internal/translate/migrate/synthesized_names_internal_test.go @@ -29,7 +29,7 @@ func TestSynthesizedNamesMarked(t *testing.T) { `, - []string{"use case def Drive {\n include use case start : Start;\n metadata MigrationMetadata::SynthesizedName about start;\n}"}}, + []string{"use case Drive {\n include Start;\n}"}}, {"an instance's slot of an anonymous feature", ` diff --git a/internal/translate/migrate/tables.go b/internal/translate/migrate/tables.go index 9b646d3d65..9a69595b73 100644 --- a/internal/translate/migrate/tables.go +++ b/internal/translate/migrate/tables.go @@ -421,7 +421,7 @@ func (m *migration) typedRows(src qx, types []sysmlv1.ElementRef, subtypes, indi merged = mergeTypeFilters(filtersToMerge) } source := src - if merged.excluding != nil && !l.perRow { + if (merged.excluding != nil || len(merged.usages) > 0) && !l.perRow { source = qshared(src) } if len(filtersToMerge) > 0 { diff --git a/internal/translate/migrate/typefilter.go b/internal/translate/migrate/typefilter.go index 3b498b4278..d644cd34f6 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -1,6 +1,7 @@ package migrate import ( + "slices" "strings" "github.com/Open-MBEE/OpenSysML/internal/translate/xmi/sysmlv1" @@ -17,6 +18,10 @@ type typeFilter struct { // excluding names a set of newly conforming elements that are not in the // original UML metaclass's result set. excluding *typeFilterExclusion + // usageKinds are the categories whose classifiers migrate to usages a + // table lists by name; usages are the plain names of those written. + usageKinds []category + usages []string // metadata is the written metadata def of a user stereotype rows carry. metadata string // all is set when the type admits every migrated element. @@ -31,6 +36,7 @@ type typeFilter struct { type v2Types struct { types []string excluding *typeFilterExclusion + usages []category note string } @@ -42,7 +48,6 @@ type typeFilterExclusion struct { // The v2 metaclass names the migrator's declarations report as @type. const ( typePartDef = "PartDefinition" - typeRequirementDef = "RequirementDefinition" typeConstraintDef = "ConstraintDefinition" typePortDef = "PortDefinition" typeAttributeDef = "AttributeDefinition" @@ -85,7 +90,7 @@ const ( ) // classTypes are the definitions a UML class of any stereotype migrates to. -var classTypes = []string{typePartDef, typeRequirementDef, typeConstraintDef, typePortDef, +var classTypes = []string{typePartDef, typeConstraintDef, typePortDef, typeVerificationDef, typeActionDef, typeStateDef, typeCalcDef, typeViewUsage, typeViewpointUsage} // propertyTypes are the usages a UML property of any type migrates to. @@ -107,14 +112,24 @@ func occurrenceAwareTypes(types []string, include, exclude, note string) v2Types } } +// withUsages adds to t the categories whose classifiers are written as usages. +func withUsages(t v2Types, usages ...category) v2Types { + t.usages = append(t.usages, usages...) + return t +} + // behaviorTypes are the definitions a UML behavior migrates to. var behaviorTypes = []string{typeActionDef, typeCalcDef, typeStateDef, typeVerificationDef} // classifierTypes are what a UML type, which is always a classifier, migrates -// to: a definition, or the view or viewpoint usage a «View» or «Viewpoint» -// class becomes. +// to by metaclass: a definition, or the view or viewpoint usage a «View» or +// «Viewpoint» class becomes. Actors and use cases become part and use case +// usages, which by type a query cannot tell from the usage a property becomes, +// so classifierUsages lists them by name instead. var classifierTypes = []string{typeDefinition, typeViewUsage, typeViewpointUsage} +var classifierUsages = []category{catActor, catUseCase, catRequirement} + // namespaceTypes add to the classifiers the packages and the states, which are // namespaces in UML; a «View» package is a view usage too. var namespaceTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeStateUsage} @@ -136,17 +151,17 @@ const ( var metaclassTypes = map[string]v2Types{ "Element": {}, "NamedElement": {note: "every migrated element is named, so a NamedElement filter admits all of them"}, - "PackageableElement": {types: packageableTypes, + "PackageableElement": {types: packageableTypes, usages: classifierUsages, note: noteClassifierExtra + "; instances of value types, written as attributes, are not"}, - "Namespace": {types: namespaceTypes, note: noteDiagramViews + + "Namespace": {types: namespaceTypes, usages: classifierUsages, note: noteDiagramViews + "; transitions and structured activity nodes are not"}, "Package": {types: []string{typePackage}}, "Model": {types: []string{typePackage}, note: "a model is a package once migrated"}, - "Type": {types: classifierTypes, note: noteClassifierExtra}, - "Classifier": {types: classifierTypes, note: noteClassifierExtra}, - "Class": occurrenceAwareTypes(classTypes, typeOccurrenceDef, typeItemDef, ""), + "Type": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, + "Classifier": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, + "Class": withUsages(occurrenceAwareTypes(classTypes, typeOccurrenceDef, typeItemDef, ""), catRequirement), "Component": occurrenceAwareTypes([]string{typePartDef}, typeOccurrenceDef, typeItemDef, "a component is a part def once migrated, as a block is"), - "Actor": occurrenceAwareTypes([]string{typePartDef}, typeOccurrenceDef, typeItemDef, "an actor is a part def once migrated, as a block is"), + "Actor": {usages: []category{catActor}}, "Behavior": {types: behaviorTypes}, "Activity": {types: []string{typeActionDef, typeCalcDef}}, "OpaqueBehavior": {types: []string{typeActionDef, typeCalcDef}}, @@ -154,7 +169,7 @@ var metaclassTypes = map[string]v2Types{ "Interaction": {types: []string{typeActionDef}}, "StateMachine": {types: []string{typeStateDef}}, "Operation": {types: []string{typeActionDef}, note: "an operation is an action def once migrated, as an activity is"}, - "UseCase": {types: []string{typeUseCaseDef}}, + "UseCase": {usages: []category{catUseCase}}, "DataType": {types: []string{typeAttributeDef, typeEnumDef}}, "PrimitiveType": {types: []string{typeAttributeDef}}, "Enumeration": {types: []string{typeEnumDef}}, @@ -193,8 +208,8 @@ var actionMetaclasses = map[string]bool{"ActivityNode": true, "ExecutableNode": // it migrates to. var stereotypeTypes = map[string]v2Types{ "Block": occurrenceAwareTypes([]string{typePartDef}, typeOccurrenceDef, typeItemDef, ""), - "Requirement": {types: []string{typeRequirementDef}}, - "AbstractRequirement": {types: []string{typeRequirementDef}}, + "Requirement": {usages: []category{catRequirement}}, + "AbstractRequirement": {usages: []category{catRequirement}}, "ConstraintBlock": {types: []string{typeConstraintDef}}, "InterfaceBlock": {types: []string{typePortDef}}, "ValueType": {types: []string{typeAttributeDef, typeEnumDef}}, @@ -244,9 +259,9 @@ func (m *migration) typeFilter(ref sysmlv1.ElementRef) typeFilter { if doc, name, ok := standardHref(e.Href); ok { switch { case doc == "UML": - return metaclassFilter(name) - case stereotypeTypes[name].types != nil: - return fromTypes("«"+name+"»", stereotypeTypes[name]) + return m.nameUsages(metaclassFilter(name)) + case stereotypeTypes[name].types != nil || stereotypeTypes[name].usages != nil: + return m.nameUsages(fromTypes("«"+name+"»", stereotypeTypes[name])) default: return typeFilter{label: "«" + name + "»", refused: "no v2 metaclass stands for the elements of «" + name + "»"} } @@ -273,7 +288,7 @@ func (m *migration) proxyTypeFilter(e *sysmlv1.Element) typeFilter { s := m.model.StereotypeRef(e.ID) if s.Name != "" && isStandardNamespace(s.Namespace) { if t, ok := stereotypeTypes[s.Name]; ok { - return fromTypes("«"+s.Name+"»", t) + return m.nameUsages(fromTypes("«"+s.Name+"»", t)) } return typeFilter{label: "«" + s.Name + "»", refused: "no v2 metaclass stands for the elements of «" + s.Name + "»"} } @@ -287,7 +302,7 @@ func (m *migration) proxyTypeFilter(e *sysmlv1.Element) typeFilter { return typeFilter{label: e.Href, refused: elementTypeSubject + e.Href + " is in a module the archive does not describe"} } if t, ok := stereotypeTypes[name]; ok && customization { - return fromTypes("«"+name+"»", t) + return m.nameUsages(fromTypes("«"+name+"»", t)) } if subs := m.specializers(e); len(subs) > 0 { return typeFilter{classifiers: subs, label: qualifiedName(e), @@ -299,7 +314,7 @@ func (m *migration) proxyTypeFilter(e *sysmlv1.Element) typeFilter { // stereotypeTypeFilter decides how a stereotype of the document filters rows. func (m *migration) stereotypeTypeFilter(e *sysmlv1.Element) typeFilter { if t, ok := stereotypeTypes[e.Name]; ok && m.isLibrary(e) && libraryRoots[pathRoot(qualifiedName(e))] { - return fromTypes("«"+e.Name+"»", t) + return m.nameUsages(fromTypes("«"+e.Name+"»", t)) } if m.userStereotype(e) && m.written(e) { return typeFilter{label: "«" + e.Name + "»", metadata: m.plainName(e)} @@ -347,34 +362,77 @@ func metaclassFilter(name string) typeFilter { if !ok { return typeFilter{label: name, refused: "no v2 metaclass stands for the elements of a UML " + name} } - if t.types == nil { + if t.types == nil && t.usages == nil { return typeFilter{label: name, all: true, note: t.note} } return fromTypes(name, t) } +// nameUsages fills in the written classifiers of the filter's usage kinds, +// which the query lists by name since their type is a property's too. +func (m *migration) nameUsages(f typeFilter) typeFilter { + if len(f.usageKinds) == 0 { + return f + } + var walk func(e *sysmlv1.Element) + walk = func(e *sysmlv1.Element) { + if cat, _ := m.classify(e); slices.Contains(f.usageKinds, cat) && m.written(e) { + f.usages = append(f.usages, m.plainName(e)) + } + for _, c := range e.Children { + walk(c) + } + } + for _, r := range m.model.Roots { + if !m.isLibrary(r) { + walk(r) + } + } + return f +} + func fromTypes(label string, t v2Types) typeFilter { - return typeFilter{label: label, types: t.types, excluding: t.excluding, note: t.note} + return typeFilter{label: label, types: t.types, excluding: t.excluding, usageKinds: t.usages, note: t.note} } +// query selects from src the rows of the filter's types, less the excluded +// ones, and the rows among its named usages. func (f typeFilter) query(src qx) qx { - rows := whereType(src, f.types...) - if f.excluding == nil { + var rows qx + typed := len(f.types) > 0 + if typed { + rows = whereType(src, f.types...) + if f.excluding != nil { + outside := qcall("Except", + qarg1("source", whereType(src, f.excluding.source...)), + qarg1("exclude", whereType(src, f.excluding.keep...))) + rows = qcall("Except", qarg1("source", rows), qarg1("exclude", outside)) + } + } + if len(f.usages) == 0 { + if !typed { + // Named takes at least one name; no usage written means no row. + return qempty() + } return rows } - outside := qcall("Except", - qarg1("source", whereType(src, f.excluding.source...)), - qarg1("exclude", whereType(src, f.excluding.keep...))) - return qcall("Except", - qarg1("source", rows), - qarg1("exclude", outside)) + named := qcall("Named", qstrs("qualifiedName", f.usages...)) + // Named ∩ src, as the difference of named and what of it is outside src. + usages := qcall("Except", + qarg1("source", named), + qarg1("exclude", qcall("Except", qarg1("source", named), qarg1("exclude", src)))) + if !typed { + return usages + } + return qcall("Union", qarg1("source", rows), qarg1("other", usages)) } func mergeTypeFilters(filters []typeFilter) typeFilter { - var types, source, keep uniqueNames + var types, source, keep, usages uniqueNames var excluding bool for _, f := range filters { types.add(f.types...) + usages.add(f.usages...) if f.excluding != nil { excluding = true source.add(f.excluding.source...) @@ -383,7 +441,7 @@ func mergeTypeFilters(filters []typeFilter) typeFilter { keep.add(f.types...) } } - merged := typeFilter{types: types} + merged := typeFilter{types: types, usages: usages} if excluding { merged.excluding = &typeFilterExclusion{source: source, keep: keep} } diff --git a/internal/translate/migrate/typefilter_internal_test.go b/internal/translate/migrate/typefilter_internal_test.go index 18ea0be147..9038821ef8 100644 --- a/internal/translate/migrate/typefilter_internal_test.go +++ b/internal/translate/migrate/typefilter_internal_test.go @@ -11,7 +11,7 @@ func TestMetaclassFiltersExcludeNewConformingTypes(t *testing.T) { }{ { name: "Class", - want: `Except(source = WhereType(source = row, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), exclude = Except(source = WhereType(source = row, type = ("ItemDefinition")), exclude = WhereType(source = row, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage"))))`, + want: `Except(source = WhereType(source = row, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), exclude = Except(source = WhereType(source = row, type = ("ItemDefinition")), exclude = WhereType(source = row, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage"))))`, }, { name: "Property", @@ -50,7 +50,7 @@ func TestMergedTypeFiltersCombineExclusions(t *testing.T) { fromTypes("«Block»", stereotypeTypes["Block"]), }) wantTypes := []string{ - typePartDef, typeRequirementDef, typeConstraintDef, typePortDef, typeVerificationDef, + typePartDef, typeConstraintDef, typePortDef, typeVerificationDef, typeActionDef, typeStateDef, typeCalcDef, typeViewUsage, typeViewpointUsage, typeOccurrenceDef, typeAttributeUsage, typePartUsage, typeItemUsage, typeReferenceUsage, typePortUsage, typeConstraintUsage, typeRequirementUse, typeActionUsage, typeStateUsage, @@ -66,7 +66,7 @@ func TestMergedTypeFiltersCombineExclusions(t *testing.T) { t.Errorf("merged exclusion source = %v, want %v", merged.excluding.source, want) } wantKeep := []string{ - typePartDef, typeRequirementDef, typeConstraintDef, typePortDef, typeVerificationDef, + typePartDef, typeConstraintDef, typePortDef, typeVerificationDef, typeActionDef, typeStateDef, typeCalcDef, typeViewUsage, typeViewpointUsage, typeAttributeUsage, typePartUsage, typeItemUsage, typeReferenceUsage, typePortUsage, typeConstraintUsage, typeRequirementUse, typeActionUsage, typeStateUsage, diff --git a/internal/translate/migrate/uniqueness.go b/internal/translate/migrate/uniqueness.go index d9ad2b411f..a27f5d0e36 100644 --- a/internal/translate/migrate/uniqueness.go +++ b/internal/translate/migrate/uniqueness.go @@ -40,7 +40,7 @@ var nestedOwners = map[category]semantics.NestedOwner{ catAttributeDef: {Def: ast.DefAttribute, IsDef: true}, catEnumDef: {Def: ast.DefEnumeration, IsDef: true}, catConstraintDef: {Def: ast.DefConstraint, IsDef: true}, - catRequirementDef: {Def: ast.DefRequirement, IsDef: true}, + catRequirement: {Usage: ast.UsageRequirement}, catConnectionDef: {Def: ast.DefConnection, IsDef: true}, catIndividualDef: {Def: ast.DefIndividual, IsDef: true}, catVerificationDef: {Def: ast.DefVerificationCase, IsDef: true}, @@ -49,7 +49,8 @@ var nestedOwners = map[category]semantics.NestedOwner{ catSimConfig: {Def: ast.DefAction, IsDef: true}, catCalcDef: {Def: ast.DefCalc, IsDef: true}, catStateDef: {Def: ast.DefState, IsDef: true}, - catUseCaseDef: {Def: ast.DefUseCase, IsDef: true}, + catUseCase: {Usage: ast.UsageUseCase}, + catActor: {Usage: ast.UsagePart}, catMetadataDef: {Def: ast.DefMetadata, IsDef: true}, catView: {Usage: ast.UsageView}, catViewpoint: {Def: ast.DefViewpoint, IsDef: true}, diff --git a/internal/translate/migrate/usecase.go b/internal/translate/migrate/usecase.go index 5db4f51dbb..820c3ca4b0 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -6,14 +6,369 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/translate/xmi/sysmlv1" ) -// actorLink is an association between a use case and an actor, written as an -// actor usage of the use case def rather than as a connection def. +// actorLink is an association between a use case and an actor, written as a +// connection between the two usages rather than as member-end properties. type actorLink struct { assoc, useCase, actor *sysmlv1.Element - // end is the association's member end typed by the actor, whose name the - // actor usage takes; name is the usage's name once placed. - end *sysmlv1.Element - name string + // end is the association's member end typed by the actor. + end *sysmlv1.Element + // conn is the connection written for the association, nil when no package holds it. + conn *defConn + // block marks a link to a block rather than a use case: its ends are the + // parts the association already writes, so only the connection is added. + block bool +} + +// defUsage is the usage a connection joins for a v1 classifier: the classifier +// itself when it is written as a usage, as an actor or use case is, else a +// usage of the definition written in a package, since a connection joins +// features, which a requirement or block definition is not. +type defUsage struct { + def, host *sysmlv1.Element + keyword, name string + // self marks def as itself the usage, written where it is in the model. + self bool +} + +// defConn is a connection written in a package between two definitions' usages, +// standing for an actor association or a «Refine» a diagram draws as a line. +type defConn struct { + // of is the v1 relationship the connection is written for. + of, host *sysmlv1.Element + name string + from, to *defUsage + // def is the association written as the connection def typing the connection, nil for an untyped one. + def *sysmlv1.Element + // doc is what the connection's body documents: where and when an extend applies. + doc string +} + +// target is the report target of the connection. +func (c *defConn) target(m *migration) string { + return m.qualified(append(m.segments(c.host), c.name)) +} + +// refineConnectionNote is the note of a «Refine» written as a connection. +const refineConnectionNote = "written as a connection between usages of its ends, which a diagram draws; the standard Refinement metadata annotates only a dependency, so the connection's name says it refines" + +// usageKeyword is the keyword of a usage of a written definition a connection +// may join, "" for an element that is no such definition. +func (m *migration) usageKeyword(def *sysmlv1.Element) string { + if !m.written(def) { + return "" + } + cat, _ := m.classify(def) + switch cat { + case catPartDef: + return "part" + case catUseCase: + return "use case" + case catActor: + return "part" + case catRequirement: + return "requirement" + case catActionDef: + return "action" + case catOccurrenceDef: + return "occurrence" + case catItemDef: + return "item" + } + return "" +} + +// usageHost is the written package or model in or above e whose body holds the +// usages a connection written for e joins; nil when no ancestor is one. +func (m *migration) usageHost(e *sysmlv1.Element) *sysmlv1.Element { + for p := e.Parent; p != nil; p = p.Parent { + if (p.Type == "Package" || p.Type == "Model") && m.written(p) { + return p + } + } + return nil +} + +// defUsageOf is the usage a connection in host's body joins for def: def +// itself when it is written as a usage, else a usage of it declared in host's +// body on first demand. +func (m *migration) defUsageOf(host, def *sysmlv1.Element, keyword string) *defUsage { + if u := m.defUsages[host][def]; u != nil { + return u + } + if m.defUsages[host] == nil { + m.defUsages[host] = map[*sysmlv1.Element]*defUsage{} + } + if m.selfUsage(def) { + u := &defUsage{def: def, host: def.Parent, keyword: keyword, name: m.nameOf(def), self: true} + m.defUsages[host][def] = u + return u + } + base := lowerFirst(def.Name) + if base == "" { + base = lowerFirst(m.nameFor(def)) + } + u := &defUsage{def: def, host: host, keyword: keyword, name: m.freshName(host, base)} + m.defUsages[host][def] = u + m.extras[host] = append(m.extras[host], func() { + m.w.line(keyword + " " + writeName(u.name) + " : " + m.ref(def, host) + ";") + }) + return u +} + +// selfUsage says whether def is written as a usage a connection joins directly. +func (m *migration) selfUsage(def *sysmlv1.Element) bool { + cat, _ := m.classify(def) + return cat == catUseCase || cat == catActor || cat == catRequirement +} + +// usageIn is the one usage written for def, which a view draws for it; nil for none. +func (m *migration) usageIn(def *sysmlv1.Element) *defUsage { + return m.defUsages[m.usageHost(def)][def] +} + +// connectUsages writes, in host's body, a connection named after e between the +// usages of the two classifiers, and records it as the edge e is drawn as. +func (m *migration) connectUsages(e, host, from, to *sysmlv1.Element, fromKw, toKw, name string) *defConn { + return m.connectTyped(e, nil, host, from, to, fromKw, toKw, name) +} + +// connectTyped is connectUsages with the connection typed by def, the +// connection def written for e, when def is not nil. +func (m *migration) connectTyped(e, def, host, from, to *sysmlv1.Element, fromKw, toKw, name string) *defConn { + fu, tu := m.defUsageOf(m.usageHost(from), from, fromKw), m.defUsageOf(m.usageHost(to), to, toKw) + if name == "" { + name = spoken(writeName(fu.name)) + " to " + spoken(writeName(tu.name)) + } + if def != nil { + // The def keeps e's name; the usage must not. + name = m.freshName(host, name) + } else { + name = m.claimName(e, host, name) + } + c := &defConn{of: e, host: host, name: name, from: fu, to: tu, def: def} + if m.conns[e] == nil { + m.conns[e] = c + } else { + m.moreConns[e] = append(m.moreConns[e], c) + } + m.extras[host] = append(m.extras[host], func() { + typed := "" + if def != nil { + typed = " : " + m.ref(def, host) + } + header := "connection " + writeName(name) + typed + " connect " + m.usageRef(fu, host) + " to " + m.usageRef(tu, host) + m.w.trailed(header, ";", "", func() { + if def != nil { + return // the connection def carries the association's metadata + } + if c.doc != "" { + m.w.line("doc /* " + c.doc + " */") + } + saved := m.scope + m.scope = host + m.metadataUsages(e) + m.scope = saved + }) + }) + return c +} + +// usageRef writes a reference to usage u from inside scope's body. +func (m *migration) usageRef(u *defUsage, scope *sysmlv1.Element) string { + if u.self { + return m.ref(u.def, scope) + } + return m.hostedRef(u.host, u.name, scope) +} + +// connectActor writes the connection a link's association is written as, +// between the actor's part usage and the use case usage, in the nearest +// package holding both, and reports the ends the association owns as its ends. +func (m *migration) connectActor(link *actorLink) { + host, ok := m.connHost(link.assoc, link.actor, link.useCase) + if !ok { + return + } + from, to, fromKw, toKw := link.actor, link.useCase, "part", m.usageKeyword(link.useCase) + if ends := m.model.Refs(link.assoc, "memberEnd"); len(ends) > 0 && ends[0] != link.end { + // The connection's ends follow the def's, so the use case end comes first. + from, to, fromKw, toKw = to, from, toKw, fromKw + } + name := m.nameOf(link.assoc) + var def *sysmlv1.Element + if m.associationAsConnectionDef(link.assoc) { + // The association is a connection def; its one usage is named like a usage. + def, name = link.assoc, lowerFirst(name) + } + link.conn = m.connectTyped(link.assoc, def, host, from, to, fromKw, toKw, name) + if link.block { + return + } + target := link.conn.target(m) + for _, end := range m.model.Refs(link.assoc, "memberEnd") { + if end.Parent != link.assoc { + continue + } + note := "" + if end == link.end { + _, note = m.multiplicity(end) + note = joinNotes(note, "the end at the actor is the connection's end at the actor's part usage") + } else { + note = "the end at the use case is the connection's end at the use case usage" + } + m.add(end, verdictFor(note), target, note) + } +} + +// usageConnection writes a relationship d between two classifiers as a +// connection between their usages in the nearest package holding both; nil +// when either end is no classifier a usage can be written of. +func (m *migration) usageConnection(d *sysmlv1.Element, name string, client, supplier *sysmlv1.Element) *defConn { + ck, sk := m.usageKeyword(client), m.usageKeyword(supplier) + if ck == "" || sk == "" { + return nil + } + host, ok := m.connHost(d, client, supplier) + if !ok { + return nil + } + return m.connectUsages(d, host, client, supplier, ck, sk, name) +} + +// connRefs are the references a view exposing e also exposes so the connection +// written for it is drawn: the connection and the two usages it joins. +func (m *migration) connRefs(e, scope *sysmlv1.Element) []string { + var refs []string + for _, c := range m.connsOf(e) { + refs = append(refs, m.hostedRef(c.host, c.name, scope), m.usageRef(c.from, scope), m.usageRef(c.to, scope)) + } + return refs +} + +// connsOf are the connections written for e: one per pair of ends a +// dependency with several clients or suppliers joins. +func (m *migration) connsOf(e *sysmlv1.Element) []*defConn { + c := m.conns[e] + if c == nil { + return nil + } + return append([]*defConn{c}, m.moreConns[e]...) +} + +// pairConn is the connection written for d between client and supplier; nil for none. +func (m *migration) pairConn(d, client, supplier *sysmlv1.Element) *defConn { + for _, c := range m.connsOf(d) { + if c.from.def == client && c.to.def == supplier { + return c + } + } + return nil +} + +// planConnections writes, before any view is, the connection each include, +// extend and «Refine» between classifiers is drawn as, so a view exposes the +// usages the connections join wherever in the model they are hosted. An +// unnamed relationship's connection is named by its kind, "A includes B": a +// comment would be a note to the tools that draw the connection. +func (m *migration) planConnections(rels []*sysmlv1.Element) { + for _, r := range rels { + var from, to *sysmlv1.Element + verb := "" + switch r.Type { + case "Include": + from, to, verb = r.Parent, m.model.Ref(r, "addition"), "includes" + if to != nil && m.written(to) { + if cat, _ := m.classify(to); cat != catUseCase { + continue + } + } + case "Extend": + from, to, verb = r.Parent, m.model.Ref(r, "extendedCase"), "extends" + default: + clients, suppliers := m.model.Refs(r, "client"), m.model.Refs(r, "supplier") + if len(clients) != 1 || len(suppliers) != 1 { + continue + } + from, to, verb = clients[0], suppliers[0], "refines" + } + if from == nil || to == nil || !m.written(from) || !m.written(to) { + continue + } + c := m.kindConnection(r, m.nameOf(r), from, to, verb) + if c == nil { + continue + } + if detail := m.extensionDetail(r); detail != "" { + if c.doc == "" { + c.doc = "extends" + } + c.doc += " " + detail + } + } +} + +// kindConnection writes the connection a relationship of a kind is drawn as, +// named "A refines B" by its kind unless its author named it, when the kind +// is the connection's doc instead. +func (m *migration) kindConnection(r *sysmlv1.Element, name string, from, to *sysmlv1.Element, verb string) *defConn { + kind := spoken(writeName(m.nameFor(from))) + " " + verb + " " + spoken(writeName(m.nameFor(to))) + if m.nameOf(r) == "" { + name = kind + } + c := m.usageConnection(r, name, from, to) + if c != nil && name != kind { + c.doc = kind + } + return c +} + +// extendComment is the comment an extend's connection or dependency carries: +// the extension points and condition it applies. +func (m *migration) extendComment(ext *sysmlv1.Element) string { + comment := "extends" + if detail := m.extensionDetail(ext); detail != "" { + comment += " " + detail + } + return comment +} + +// connOf is the connection e is drawn as: the one written for e, or for the +// association e is a member end of; nil for none. +func (m *migration) connOf(e *sysmlv1.Element) *defConn { + if c := m.conns[e]; c != nil { + return c + } + if a := m.associationEnds[e]; a != nil { + return m.conns[a] + } + return nil +} + +// connHost is the package a connection between usages of a and b is written +// in (nil for the top level): the nearest package holding both, so each +// definition has one usage there however many relationships join it; else the +// package d is in. ok is false when there is none. +func (m *migration) connHost(d, a, b *sysmlv1.Element) (host *sysmlv1.Element, ok bool) { + above := map[*sysmlv1.Element]bool{} + for p := a.Parent; p != nil; p = p.Parent { + above[p] = true + } + for p := b.Parent; p != nil; p = p.Parent { + switch { + case !above[p]: + case isTopLevel(p): + return nil, true + case (p.Type == "Package" || p.Type == "Model") && m.written(p): + return p, true + } + } + host = m.usageHost(d) + return host, host != nil +} + +// hostedRef refers from scope to name, declared in host's body (the top level for nil). +func (m *migration) hostedRef(host *sysmlv1.Element, name string, scope *sysmlv1.Element) string { + return m.refMember(host, name, namespaces(append(m.segments(host), name)), scope, false) } // actorLink recognises an association linking a written use case to a written @@ -29,7 +384,13 @@ func (m *migration) actorLink(a *sysmlv1.Element) *actorLink { for i, end := range ends { actor := m.model.Ref(end, "type") useCase := m.model.Ref(ends[1-i], "type") - if actor == nil || useCase == nil || actor.Type != "Actor" || useCase.Type != "UseCase" { + if actor == nil || useCase == nil || actor.Type != "Actor" { + continue + } + if useCase.Type != "UseCase" { + if m.usageKeyword(useCase) == "part" && m.usageKeyword(actor) == "part" { + return &actorLink{assoc: a, useCase: useCase, actor: actor, end: end, block: true} + } continue } if end.Parent == useCase || !m.written(actor) || !m.written(useCase) { @@ -40,46 +401,19 @@ func (m *migration) actorLink(a *sysmlv1.Element) *actorLink { return nil } -// placeActors names the actor usage each link is written as, once every member -// of the use cases is named, and registers it in the use case's body. +// placeActors writes the connection each link's association is written as, +// once every member of the use cases is named, and records the link. func (m *migration) placeActors(links []*actorLink) { for _, link := range links { - base := m.nameOf(link.end) - if base == "" { - base = lowerFirst(link.actor.Name) - } - link.name = m.freshName(link.useCase, base) - m.actors[link.assoc] = link - m.extras[link.useCase] = append(m.extras[link.useCase], func() { m.actorMember(link) }) - } -} - -// actorMember writes the actor usage of a link in its use case's body and -// reports the association's ends it stands for. -func (m *migration) actorMember(link *actorLink) { - decl := "actor " + writeName(link.name) + " : " + m.ref(link.actor, link.useCase) - mult, mnote := m.multiplicity(link.end) - m.w.line(decl + mult + ";") - target := m.actorTarget(link) - for _, end := range m.model.Refs(link.assoc, "memberEnd") { - if end.Parent != link.assoc { - continue - } - note := mnote - if end != link.end { - note = "the end at the use case is the use case def the actor is declared in" + if !link.block { + m.actors[link.assoc] = link } - m.add(end, verdictFor(note), target, note) + m.connectActor(link) } } -// actorTarget is the report target of a link's actor usage. -func (m *migration) actorTarget(link *actorLink) string { - return m.qualified(append(m.segments(link.useCase), link.name)) -} - -// useCaseBody writes the body of a use case def: its subject, the actors its -// associations link it to, its members and inclusions, and its classifier behavior. +// useCaseBody writes the body of a use case usage: its subject, its members +// and inclusions, and its classifier behavior. func (m *migration) useCaseBody(e *sysmlv1.Element) { saved := m.scope m.scope = e @@ -127,14 +461,9 @@ func (m *migration) subjects(e *sysmlv1.Element) { } } -// hasActors reports whether an actor usage is written in the use case's body: -// for an association reaching an actor, or for a property typed by one. +// hasActors reports whether an actor usage is written in the use case's body, +// for a property typed by an actor. func (m *migration) hasActors(useCase *sysmlv1.Element) bool { - for _, link := range m.actors { - if link.useCase == useCase { - return true - } - } for _, p := range useCase.Owned("ownedAttribute") { t := m.model.Ref(p, "type") if p.Type != "Port" && t != nil && t.Type == "Actor" && m.written(t) { @@ -145,7 +474,7 @@ func (m *migration) hasActors(useCase *sysmlv1.Element) bool { } // include writes a UML Include as an include use case usage of the including -// use case, named after the included one. +// use case referring to the included one's usage. func (m *migration) include(inc *sysmlv1.Element) { added := m.model.Ref(inc, "addition") switch { @@ -157,19 +486,20 @@ func (m *migration) include(inc *sysmlv1.Element) { m.unmapped(inc, joinNotes("the included use case "+qualifiedName(added)+" is not migrated", why)) return } - if cat, _ := m.classify(added); cat != catUseCaseDef { - m.unmapped(inc, "the included element "+qualifiedName(added)+" becomes a "+cat.keyword()+", which an include use case cannot be typed by") + if cat, _ := m.classify(added); cat != catUseCase { + m.unmapped(inc, "the included element "+qualifiedName(added)+" becomes a "+cat.keyword()+", which an include use case cannot refer to") return } - name := m.nameOf(inc) - if name == "" { - name = m.freshName(inc.Parent, lowerFirst(m.nameFor(added))) - m.names[inc], m.synthesized[inc] = name, true + m.wroteEdge(inc, m.scope, "include", "") + m.w.line("include " + m.ref(added, m.scope) + ";") + verdict, note := Mapped, "" + if name := m.nameOf(inc); name != "" { + verdict, note = Approximated, "an include referring to a use case usage has no name, so "+name+" is dropped" + } + if conn := m.conns[inc]; conn != nil { + m.wroteEdgeAlso(inc, conn.host, "connection", nil, conn.name) } - m.madeUp(inc, writeName(name)) - m.wroteEdge(inc, m.scope, "include", name) - m.w.line("include use case " + writeName(name) + " : " + m.ref(added, m.scope) + ";") - m.add(inc, Mapped, m.v2Name(inc), "") + m.add(inc, verdict, m.qualified(m.segments(inc.Parent)), note) m.stereotypeComments(inc) } @@ -187,11 +517,16 @@ func (m *migration) extend(ext *sysmlv1.Element) { return } note := "v2 has no extend: the extension is written as a plain dependency on the extended use case" - comment := "extends" + comment := m.extendComment(ext) if detail := m.extensionDetail(ext); detail != "" { - comment += " " + detail note += "; it applies " + detail + ", which only a comment keeps" } + if conn := m.conns[ext]; conn != nil { + m.wroteEdgeAlso(ext, conn.host, "connection", nil, conn.name) + m.add(ext, Approximated, conn.target(m), strings.Replace(note, "a plain dependency on", "a connection between usages of the extending use case and", 1)) + m.stereotypeComments(ext) + return + } from, to := m.ref(ext.Parent, m.scope), m.ref(extended, m.scope) decl, target := "dependency ", "" name := m.nameOf(ext) diff --git a/internal/translate/migrate/usecase_internal_test.go b/internal/translate/migrate/usecase_internal_test.go index b100d75f4a..be709ef538 100644 --- a/internal/translate/migrate/usecase_internal_test.go +++ b/internal/translate/migrate/usecase_internal_test.go @@ -31,16 +31,16 @@ func TestUseCaseForms(t *testing.T) { id string verdict Verdict }{ - {"a use case with a subject is a use case def", + {"a use case with a subject is a use case usage", ``, "", - []string{"use case def Buy {\n subject shop : Shop;\n metadata MigrationMetadata::SynthesizedName about shop;\n}"}, "_uc", Mapped}, + []string{"use case Buy {\n subject shop : Shop;\n metadata MigrationMetadata::SynthesizedName about shop;\n}"}, "_uc", Mapped}, {"an incidental stereotype does not change the form", ``, ``, - []string{"use case def Buy {\n /* applied stereotype «HyperlinkOwner» */\n}"}, "_uc", Mapped}, + []string{"use case Buy {\n /* applied stereotype «HyperlinkOwner» */\n}"}, "_uc", Mapped}, {"a second subject is a reference usage", ``, "", - []string{"subject shop : Shop;\n ref part user : User;"}, "_uc", Approximated}, + []string{"subject shop : Shop;\n ref part user :> User;"}, "_uc", Approximated}, {"subjects written as usages are subset, not typed", ` @@ -53,33 +53,33 @@ func TestUseCaseForms(t *testing.T) { `, ``, - []string{"use case def Review {\n subject system : Report::System;\n metadata MigrationMetadata::SynthesizedName about system;\n}"}, "_uc", Mapped}, - {"an association-owned actor end is an actor of the use case", + []string{"use case Review {\n subject system : Report::System;\n metadata MigrationMetadata::SynthesizedName about system;\n}"}, "_uc", Mapped}, + {"an association-owned actor end is a connection between the actor and the use case", ` `, "", - []string{"use case def Buy {\n subject;\n actor user : User;\n}"}, "_a", Mapped}, - {"an actor end the use case owns is written once, as its actor", + []string{"part User;", "use case Buy;", "connection 'User to Buy' connect User to Buy;"}, "_a", Mapped}, + {"an actor end the use case owns is its actor, subsetting the actor's part", ` `, "", - []string{"use case def Buy {\n subject;\n actor customer : User;\n}"}, "_p", Mapped}, - {"an include is an include use case usage", + []string{"use case Buy {\n subject;\n actor customer :> User;\n}"}, "_p", Mapped}, + {"an include is an include of the included use case", ` `, "", - []string{"include use case browse : Browse;"}, "_inc", Mapped}, + []string{"use case Buy {\n include Browse;\n}"}, "_inc", Mapped}, {"an include of a use case outside the document is refused", ` `, "", []string{"not migrated: Include (_inc) — the included use case is not in the document"}, "_inc", Unmapped}, - {"an extend is a dependency keeping its point and condition", + {"an extend is a connection documenting its point and condition", ` @@ -90,18 +90,18 @@ func TestUseCaseForms(t *testing.T) { `, "", - []string{"dependency Buy to Browse; /* extends at extension point(s) 'found' when true */", + []string{"connection 'Buy extends Browse' connect Buy to Browse {\n doc /* extends at extension point(s) 'found' when true */\n}", "not migrated: ExtensionPoint 'found'"}, "_ext", Approximated}, {"an extend keeps an extension point that is not in the document", ` `, "", - []string{"dependency Buy to Browse; /* extends at extension point(s) _gone (not in the document) */"}, "_ext", Approximated}, + []string{"connection 'Buy extends Browse' connect Buy to Browse {\n doc /* extends at extension point(s) _gone (not in the document) */\n}"}, "_ext", Approximated}, {"a block property typed by a use case is a reference use case usage", ` `, ``, - []string{"part def Site {\n ref use case visit : Browse;\n}"}, "_p", Approximated}, + []string{"part def Site {\n ref use case visit :> Browse;\n}"}, "_p", Approximated}, } { t.Run(tc.name, func(t *testing.T) { r, err := Migrate("cases.xmi", []byte(useCaseModel(tc.members, tc.stereotype))) @@ -145,7 +145,7 @@ func TestUseCaseSubjectFeaturedElsewhere(t *testing.T) { t.Fatal(err) } got := string(r.Notation) - if !strings.Contains(got, "use case def Review;") || strings.Contains(got, "subject overview") { + if !strings.Contains(got, "use case Review;") || strings.Contains(got, "subject overview") { t.Errorf("notation names the nested view:\n%s", got) } want := "the subject is not written: the view Report::Overview is a feature of the part def Report, which only its members can name" diff --git a/internal/translate/migrate/views.go b/internal/translate/migrate/views.go index a23f6397f5..24b760afbc 100644 --- a/internal/translate/migrate/views.go +++ b/internal/translate/migrate/views.go @@ -600,8 +600,8 @@ func (m *migration) prepareConcerns() { info.homed = !m.isLibrary(owner) && m.written(owner) } else { cat, _ := m.classify(owner) - info.homed = (cat == catPartDef || cat == catOccurrenceDef || cat == catActionDef || cat == catRequirementDef || - cat == catUseCaseDef || cat == catView || cat == catViewpoint) && m.written(owner) + info.homed = (cat == catPartDef || cat == catOccurrenceDef || cat == catActionDef || cat == catRequirement || + cat == catUseCase || cat == catActor || cat == catView || cat == catViewpoint) && m.written(owner) } } if info.homed { diff --git a/tests/migrate/classifier_usages_test.go b/tests/migrate/classifier_usages_test.go new file mode 100644 index 0000000000..2ad0e72d17 --- /dev/null +++ b/tests/migrate/classifier_usages_test.go @@ -0,0 +1,43 @@ +package migrate_test + +import ( + "strings" + "testing" +) + +// A generic table over Classifier, Type, Namespace or PackageableElement lists +// the actors and use cases of its scope, which migrate to part and use case +// usages, by name — never the use-case-typed or actor-typed properties and +// include usages that share their v2 type. Actor and UseCase tables list only +// those usages. +func TestClassifierTablesListActorAndUseCaseUsages(t *testing.T) { + s := session(t, migrateFixtureFile(t, "classifier_usages")) + + for _, name := range []string{"Classifiers", "Types", "Namespaces", "Packageable Elements"} { + got := rows(t, s, "Tables::'"+name+" Rows'") + wantInOrder(t, name+" rows", got, + "returned 4 rows", + "Operations::Console\n", "Operations::Focus\n", "Operations::Operator\n", "Operations::Scan\n") + for _, feature := range []string{"Console::current", "Console::user", "Scan::"} { + if strings.Contains(got, feature) { + t.Errorf("%s lists the owned feature %s:\n%s", name, feature, got) + } + } + } + + actors := rows(t, s, "Tables::'Actors Rows'") + wantInOrder(t, "Actors rows", actors, "returned 1 row", "Operations::Operator\n") + + cases := rows(t, s, "Tables::'Use Cases Rows'") + wantInOrder(t, "Use Cases rows", cases, "returned 2 rows", "Operations::Focus\n", "Operations::Scan\n") +} + +// Actor and UseCase tables over a model with neither list no rows, rather than +// naming nothing. +func TestEmptyActorAndUseCaseTablesExecute(t *testing.T) { + s := session(t, migrateFixtureFile(t, "empty_classifier_tables")) + for _, name := range []string{"Actors", "Use Cases"} { + got := rows(t, s, "Tables::'"+name+" Rows'") + wantInOrder(t, name+" rows", got, "returned 0 rows") + } +} diff --git a/tests/migrate/classify_test.go b/tests/migrate/classify_test.go index 05a8b676ab..0b7cdb9231 100644 --- a/tests/migrate/classify_test.go +++ b/tests/migrate/classify_test.go @@ -438,7 +438,7 @@ func TestCustomStereotypesWithStandardNamesAreNotConsumed(t *testing.T) { `, custom("Requirement", "Class", "_r")+custom("Block", "Class", "_b")+custom("ModelLibrary", "Package", "_lib")+ custom("FlowProperty", "Property", "_fp")+custom("Satisfy", "Abstraction", "_d")+``) - wantNoLine(t, r.Notation, "requirement def") + wantNoLine(t, r.Notation, "requirement Req") wantLine(t, r.Notation, "occurrence def Req {") wantLine(t, r.Notation, "applied stereotype «Requirement»") wantLine(t, r.Notation, "applied stereotype «Block»") @@ -466,7 +466,7 @@ func TestRequirementTagsComeOnlyFromStandardStereotypes(t *testing.T) { t.Run(name, func(t *testing.T) { r := migrateDocument(t, ` `, apps) - wantLine(t, r.Notation, "requirement def Req {") + wantLine(t, r.Notation, "requirement Req {") wantLine(t, r.Notation, "doc /* Shall. */") wantNoLine(t, r.Notation, "X9> Req") wantNoLine(t, r.Notation, "doc /* Custom text. */") @@ -481,7 +481,7 @@ func TestRequirementIDDropsHTML(t *testing.T) { r := migrateDocument(t, ` `, ` `) - wantLine(t, r.Notation, "requirement def <'REQ-1'> Req {") + wantLine(t, r.Notation, "requirement <'REQ-1'> Req {") wantLine(t, r.Notation, "doc /* Shall. */") for _, d := range errors(t, "req.sysml", r.Notation) { t.Errorf("%s", d.Message) @@ -512,7 +512,7 @@ func TestPapyrusProfileClassifiesAndToolCustomizationsDoNot(t *testing.T) { wantLine(t, r.Notation, "part def Pump {") wantLine(t, r.Notation, "attribute def Rate;") wantLine(t, r.Notation, "attribute throughput : Rate {") - wantLine(t, r.Notation, "requirement def Req {") + wantLine(t, r.Notation, "requirement Req {") wantLine(t, r.Notation, "applied stereotype «ValueProperty»") wantLine(t, r.Notation, "applied stereotype «performanceRequirement»") wantLine(t, r.Notation, "occurrence def Plain {") @@ -620,3 +620,42 @@ func TestSignalIsAnItemDef(t *testing.T) { t.Errorf("%v", diags) } } + +// A v1 requirement is a requirement usage, as a tool's users draw one: a nested +// requirement is a subrequirement, a satisfy, verify or refine joins it directly +// with no usage written beside it, a property it types subsets it, and an +// instance of it is no individual. +func TestRequirementsAreUsages(t *testing.T) { + r := migrateDocument(t, ` + + + + + + + + + + + + + + + `, ` + + + + + + `) + wantLine(t, r.Notation, "requirement Mass {") + wantLine(t, r.Notation, "requirement <'R1.1'> 'Chassis Mass' {") + wantLine(t, r.Notation, "requirement Safety :> Mass;") + wantNoLine(t, r.Notation, "requirement def") + wantNoLine(t, r.Notation, "requirement mass : Mass;") + wantLine(t, r.Notation, "requirement held :> Reqs::Mass;") + wantLine(t, r.Notation, "connection 'Drive refines Mass' connect Drive to Reqs::Mass;") + wantLine(t, r.Notation, "satisfy Reqs::Mass.'Chassis Mass';") + wantNote(t, r, "_i", migrate.Unmapped, "the instance's classifier Reqs::Mass is written as a requirement usage, which an individual cannot specialize") + wantClean(t, "requirement-usages.sysml", r) +} diff --git a/tests/migrate/documents_test.go b/tests/migrate/documents_test.go index 234537e751..2f088dc9c4 100644 --- a/tests/migrate/documents_test.go +++ b/tests/migrate/documents_test.go @@ -136,7 +136,7 @@ func TestMigratedTablesExecute(t *testing.T) { "Plant::Inventory::p1", `name = "p1"`, `qualifiedName = "Plant::Inventory::p1"`, `name 2 = "primary"`, `mass = 12.5`, "Plant::Inventory::p2", `name = "p2"`, `qualifiedName = "Plant::Inventory::p2"`, `name 2 = ""`) - // Generic table: every requirement definition of the scope by name. + // Generic table: every requirement of the scope by name. reqs := rows(t, s, "Plant::Requirements::'Requirement Table Rows'") wantInOrder(t, "Requirement Table rows", reqs, "returned 3 rows", @@ -182,7 +182,7 @@ func TestMigratedTablesExecute(t *testing.T) { related := rows(t, s, "Plant::Structure::'Pump Requirement Map Rows'") wantInOrder(t, "Pump Requirement Map rows", related, "returned 1 row", - "Plant::Requirements::FlowRequirement", `@type = "RequirementDefinition"`) + "Plant::Requirements::FlowRequirement", `@type = "RequirementUsage"`) // Whole-model scope filtered by a migrated user stereotype, the elements // its specializations are applied to included. @@ -862,7 +862,7 @@ func TestMigratedCollectorsAndFilters(t *testing.T) { // Requirement id and text columns read the short name and the doc. wantInOrder(t, "Specification query", notation, "calc def 'Yard Handbook Requirements Specification Table Rows'", - `type = ("RequirementDefinition")),`, + `source = DocumentQueries::Named(qualifiedName = ("Needs::Lift", "Needs::Sway")),`, `properties = ("shortName", "name", "documentation"))`) wantNote(t, r, "_st_todo_table", migrate.Unmapped, "the elements it shows pass through «FilterByStereotypes» Yard Viewpoints::To Do Viewpoint::To Do Method::Filter By Stereotypes is not migrated: no v2 metaclass stands for the elements of «TODO_Owner»") diff --git a/tests/migrate/layout_test.go b/tests/migrate/layout_test.go index d5c97ab2d4..567c060e9f 100644 --- a/tests/migrate/layout_test.go +++ b/tests/migrate/layout_test.go @@ -192,7 +192,7 @@ func TestGoldenEdgeLayout(t *testing.T) { if l == nil { t.Fatal("no layout summary") } - if l.Routes != 22 || l.RoutesWritten != 10 || l.RoutesUnexposed != 11 || l.RoutesDangling != 1 { + if l.Routes != 22 || l.RoutesWritten != 11 || l.RoutesUnexposed != 10 || l.RoutesDangling != 1 { t.Errorf("routes: %+v", l) } // An edge of the graph the activity's rendering does not draw is exposed, not swallowed. @@ -237,7 +237,7 @@ func TestGoldenEdgeLayout(t *testing.T) { {Kind: "ControlFlow", Reason: "written", Count: 2}, {Kind: "Dependency", Reason: "not drawn", Count: 2}, {Kind: "Generalization", Reason: "no v2 member", Count: 1}, - {Kind: "Include", Reason: "not drawn", Count: 1}, + {Kind: "Include", Reason: "written", Count: 1}, {Kind: "ObjectFlow", Reason: "not drawn", Count: 1}, {Kind: "ObjectFlow", Reason: "written", Count: 1}, {Kind: "Satisfy", Reason: "not drawn", Count: 2}, diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index 94d039855e..adb7318808 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -159,26 +159,26 @@ func TestNotationCoversTheFixture(t *testing.T) { "attribute mass : 'Vehicle Design'::'Value Types'::Mass default = 1200.0;", "part engine : Engine[1..2];", "part wheels : Wheel[4..*];", - "ref part driver : Driver[0..1];", + "ref part driver :> Driver[0..1];", "port fuelIn : ~'Vehicle Design'::Interfaces::FuelInterface;", "binding 'fuel line' bind fuelIn = engine.fuelPort;", "flow of 'Vehicle Design'::Interfaces::Fuel from fuelIn.fuel to engine.fuelPort.fuel;", "bind mass = massLimit.m;", "bind speedOut = engine.piston.p;", - "satisfy requirement : RequirementsModel::'Mass Requirement';", - "satisfy requirement : RequirementsModel::'Engine Mass Requirement' by engine;", + "satisfy RequirementsModel::'Mass Requirement';", + "satisfy RequirementsModel::'Engine Mass Requirement' by engine;", "part engine : Motor :>> engine;", "connection def Drives {", "constraint def MassLimit {", "m < limit", "individual part def myCar :> Vehicle {", "attribute :>> mass = 1350.5;", - "requirement def 'Mass Requirement' {", + "requirement 'Mass Requirement' {", "doc /* The vehicle shall have a mass of less than 1500 kg. */", - "requirement def <'R1.1'> 'Chassis Mass' {", + "requirement <'R1.1'> 'Chassis Mass' {", ":> RequirementDerivation::Derivation {", - "end #RequirementDerivation::derive derivedRequirement : 'Engine Mass Requirement';", - "verify requirement : 'Mass Requirement';", + "end #RequirementDerivation::derive derivedRequirement :> 'Engine Mass Requirement';", + "verify 'Mass Requirement';", "allocation def 'Motor to Engine' {", "end :>> source : 'Vehicle Design'::Motor;", "end :>> target : 'Vehicle Design'::Engine;", @@ -219,7 +219,7 @@ func TestReportAccountsForEveryElement(t *testing.T) { "_act_drive": migrate.Mapped, "_op_start": migrate.Mapped, "_unit_kg": migrate.Unmapped, - "_dep_refine": migrate.Mapped, + "_dep_refine": migrate.Approximated, "_dep_verify_block": migrate.Unmapped, "_lib_sysml": migrate.Skipped, "_diag_bdd": migrate.Approximated, @@ -382,6 +382,8 @@ var constructFixtures = []string{ "stub_actions", "tables", "metaclass_tables", + "classifier_usages", + "empty_classifier_tables", "documents", "figures", "collectors", diff --git a/tests/migrate/profile_test.go b/tests/migrate/profile_test.go index cd1aad8644..ea8d5b8d97 100644 --- a/tests/migrate/profile_test.go +++ b/tests/migrate/profile_test.go @@ -8,11 +8,11 @@ import ( ) // A user stereotype specializing a standard one carries its meaning: the class -// is a requirement def with the inherited Id and Text, and the user-added tags +// is a requirement with the inherited Id and Text, and the user-added tags // become a metadata usage of the user stereotype. func TestUserStereotypeSpecializingStandardOneClassifies(t *testing.T) { r := migrateFixtureFile(t, "org_profile") - wantLine(t, r.Notation, "requirement def <'REQ-1'> 'Flow Requirement' {") + wantLine(t, r.Notation, "requirement <'REQ-1'> 'Flow Requirement' {") wantLine(t, r.Notation, "doc /* The pump shall deliver 10 l/s. */") wantLine(t, r.Notation, "@'Org Profile'::'Org Requirement' {") wantLine(t, r.Notation, "criticality = 'Org Profile'::Criticality::high;") @@ -20,10 +20,10 @@ func TestUserStereotypeSpecializingStandardOneClassifies(t *testing.T) { wantLine(t, r.Notation, "weight = 2.0;") wantNoLine(t, r.Notation, "Id = ") wantNoLine(t, r.Notation, "Text = \"The pump") - wantLine(t, r.Notation, "requirement def <'REQ-2'> 'Safety Requirement' {") + wantLine(t, r.Notation, "requirement <'REQ-2'> 'Safety Requirement' {") wantLine(t, r.Notation, "part def Pump {") wantLine(t, r.Notation, "attribute def FlowRate {") - wantLine(t, r.Notation, "satisfy requirement : 'Flow Requirement' {") + wantLine(t, r.Notation, "satisfy 'Flow Requirement' {") wantLine(t, r.Notation, "@'Org Profile'::'Org Satisfy' {") wantNoLine(t, r.Notation, "applied stereotype") for _, id := range []string{"_req_flow", "_req_safe", "_pump", "_flow", "_sat"} { @@ -60,7 +60,7 @@ func TestSameNamedUserStereotypeWithoutGeneralIsNotStandard(t *testing.T) { wantLine(t, r.Notation, "occurrence def 'Design Note' {") wantLine(t, r.Notation, "@Legacy::Requirement {") wantLine(t, r.Notation, "Text = \"Kept for reference only.\";") - wantNoLine(t, r.Notation, "requirement def 'Design Note'") + wantNoLine(t, r.Notation, "requirement 'Design Note'") if es := entriesFor(r, "_note"); len(es) != 1 || es[0].Verdict != migrate.Mapped { t.Errorf("_note entries = %+v", es) } @@ -75,21 +75,26 @@ func TestInheritedStereotypeSemanticsFollowEveryGeneral(t *testing.T) { wantLine(t, r.Notation, "part def Gear {") wantLine(t, r.Notation, "@Tailoring::Diamond {") wantLine(t, r.Notation, "attribute def Torque {") - wantLine(t, r.Notation, "requirement def <'R-1'> 'Torque Requirement' {") - wantLine(t, r.Notation, "verify requirement : 'Torque Requirement' {") - wantLine(t, r.Notation, "@ModelingMetadata::Refinement;") + wantLine(t, r.Notation, "requirement <'R-1'> 'Torque Requirement' {") + wantLine(t, r.Notation, "verify 'Torque Requirement' {") + wantLine(t, r.Notation, "connection 'Spin refines Speed Requirement' connect spin to 'Speed Requirement' {") + wantNoLine(t, r.Notation, "«Refine»") wantLine(t, r.Notation, "@Tailoring::'Detailed By';") + wantNoLine(t, r.Notation, "@ModelingMetadata::Refinement;") wantLine(t, r.Notation, "connection def 'Derive Speed Requirement' :> RequirementDerivation::Derivation {") wantLine(t, r.Notation, "allocation def 'Spin to Shaft' {") wantLine(t, r.Notation, "end :>> source : Spin;") wantLine(t, r.Notation, "end :>> target : Shaft;") wantLine(t, r.Notation, "@Tailoring::'Assigned To';") wantNoLine(t, r.Notation, "applied stereotype") - for _, id := range []string{"_gear", "_torque", "_req_torque", "_verify", "_refine", "_derive", "_alloc"} { + for _, id := range []string{"_gear", "_torque", "_req_torque", "_verify", "_derive", "_alloc"} { if es := entriesFor(r, id); len(es) != 1 || es[0].Verdict != migrate.Mapped { t.Errorf("%s entries = %+v", id, es) } } + if es := entriesFor(r, "_refine"); len(es) != 1 || es[0].Verdict != migrate.Approximated || !strings.Contains(es[0].Note, "connection between usages") { + t.Errorf("_refine entries = %+v", es) + } if es := entriesFor(r, "_trace"); len(es) != 1 || es[0].Verdict != migrate.Approximated || !strings.Contains(es[0].Note, "plain dependency") { t.Errorf("_trace entries = %+v", es) } diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index 80507585e3..470d97942c 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1,6 +1,7 @@ package migrate_test import ( + "fmt" "strings" "testing" @@ -144,11 +145,11 @@ func TestMultiEndedDependenciesWriteEveryPair(t *testing.T) { "allocation def 'A to C' {", "allocation def 'A to D' {", "allocation def 'B to C' {", "allocation def 'B to D' {", "end :>> source : A;", "end :>> source : B;", "end :>> target : C;", "end :>> target : D;", - "satisfy requirement : R1;", "satisfy requirement : R2;", + "satisfy R1;", "satisfy R2;", } { wantLine(t, r.Notation, line) } - if n := strings.Count(string(r.Notation), "satisfy requirement : R1;"); n != 2 { + if n := strings.Count(string(r.Notation), "satisfy R1;"); n != 2 { t.Errorf("R1 satisfied %d times, want 2", n) } for id, want := range map[string]migrate.Verdict{"_alloc": migrate.Approximated, "_sat": migrate.Approximated, "_sat_half": migrate.Approximated} { @@ -1382,9 +1383,10 @@ const namedRelationApplications = ` func TestNamedRelationshipsKeepTheirNames(t *testing.T) { r := migrateDocument(t, namedRelationModel, namedRelationApplications) - wantLine(t, r.Notation, "satisfy requirement sat : Req by piece;") - wantLine(t, r.Notation, "verify requirement ver : Req;") - wantLine(t, r.Notation, "dependency 'ref' from Thing to Req {") + wantLine(t, r.Notation, "satisfy requirement sat :> Req by piece;") + wantLine(t, r.Notation, "verify requirement ver :> Req;") + wantLine(t, r.Notation, "connection 'ref' connect thing to Req {") + wantLine(t, r.Notation, "doc /* Thing refines Req */") wantLine(t, r.Notation, "allocation def alloc {") wantLine(t, r.Notation, "end :>> source : Thing;") wantLine(t, r.Notation, "end :>> target : Piece;") @@ -1396,7 +1398,7 @@ func TestNamedRelationshipsKeepTheirNames(t *testing.T) { }{ "_sat": {migrate.Mapped, "Thing::sat"}, "_ver": {migrate.Mapped, "Check::ver"}, - "_ref": {migrate.Mapped, "'ref'"}, + "_ref": {migrate.Approximated, "'ref'"}, "_alloc": {migrate.Mapped, "alloc"}, "_trace": {migrate.Approximated, "trace"}, "_copy": {migrate.Approximated, "copy"}, @@ -1422,7 +1424,7 @@ func TestPlacedRelationshipNameYieldsToAMember(t *testing.T) { `) - wantLine(t, r.Notation, "satisfy requirement 'sat 2' : Req by sat;") + wantLine(t, r.Notation, "satisfy requirement 'sat 2' :> Req by sat;") if es := entriesFor(r, "_sat"); len(es) != 1 || es[0].Verdict != migrate.Approximated || !strings.Contains(es[0].Note, "sat 2") { t.Errorf("entries = %+v", es) } @@ -1676,3 +1678,146 @@ func TestItemFlowWithEndsStandingForBothIsNotMigrated(t *testing.T) { } wantClean(t, "t.sysml", r) } + +// An actor or use case is itself a usage a connection joins; a definition gets +// a package-level usage only when a connection joins it: a v1 association, +// include, extend or «Refine» names it as an end. +func TestUsagesOnlyForConnectedDefinitions(t *testing.T) { + r := migrateFixtureFile(t, "parking_usecases") + for _, line := range []string{"part Driver {", "part Bank;", "use case 'Park Car' {", "use case 'Pay Fee'"} { + wantLine(t, r.Notation, line) + } + for _, line := range []string{"part driver :", "part bank :", "use case 'park Car' :", "use case 'pay Fee' :"} { + wantNoLine(t, r.Notation, line) + } + for _, line := range []string{"part garage :", "part ticket : Ticket;", "part 'valet Service' :", "use case 'valet Park' :"} { + wantNoLine(t, r.Notation, line) + } + // A named association is a connection def with one usage typed by it, named like a usage. + wantLine(t, r.Notation, "connection settlement : 'Use Cases'::Settlement connect Context::Bank to 'Use Cases'::'Pay Fee';") + wantNoLine(t, r.Notation, "connection Settlement ") + v := migrateFixtureFile(t, "vehicle") + wantLine(t, v.Notation, "connection drives : Drives connect Driver to vehicle;") + wantNoLine(t, v.Notation, "connection Drives ") +} + +const refineEndsModel = ` + + + ` + +const refineEndsApplications = ` + + ` + +// A «Refine» with several clients is one connection per client–supplier pair. +func TestRefineWithSeveralEndsIsAConnectionPerPair(t *testing.T) { + r := migrateDocument(t, refineEndsModel+` + `, refineEndsApplications) + wantLine(t, r.Notation, "connection 'Alpha refines Req' connect Alpha to Req;") + wantLine(t, r.Notation, "connection 'Beta refines Req' connect Beta to Req;") + wantNoLine(t, r.Notation, "«Refine»") + got := entriesFor(r, "_ref") + if len(got) != 1 || got[0].Verdict != migrate.Approximated || !strings.Contains(got[0].Note, "written as 2 relationships, one per client–supplier pair") { + t.Errorf("_ref: entries = %+v, want one approximated entry written as 2 relationships", got) + } + // A named refine keeps its name on the first pair and numbers the others. + r = migrateDocument(t, refineEndsModel+` + `, refineEndsApplications) + wantLine(t, r.Notation, "connection Link connect Alpha to Req {") + wantLine(t, r.Notation, "doc /* Alpha refines Req */") + wantLine(t, r.Notation, "connection 'Link 2' connect Beta to Req {") + wantLine(t, r.Notation, "doc /* Beta refines Req */") + wantNoLine(t, r.Notation, "connection 'Link 2 2'") +} + +// A connection named after its relationship yields to a member of the package +// it is written in that already has that name. +func TestConnectionNameYieldsToAPackageMember(t *testing.T) { + r := migrateDocument(t, refineEndsModel+` + + `, refineEndsApplications) + wantLine(t, r.Notation, "use case Link;") + wantLine(t, r.Notation, "connection 'Link 2' connect Alpha to Req {") + wantNoLine(t, r.Notation, "connection Link ") + if got := entriesFor(r, "_ref"); len(got) != 1 || got[0].Verdict != migrate.Approximated || got[0].Target != "'Link 2'" { + t.Errorf("_ref: entries = %+v, want one approximated 'Link 2'", got) + } + // An association's connection def keeps its name; the usage typed by it yields. + r = migrateDocument(t, ` + + + + + + `, "") + wantLine(t, r.Notation, "connection def drives {") + wantLine(t, r.Notation, "connection 'drives 2' : drives connect Driver to Drive;") +} + +const actorEndModel = ` + + + + + + + + + + + + + + + %s + + + + + + ` + +func viewBody(t *testing.T, notation []byte, name string) string { + t.Helper() + s := string(notation) + i := strings.Index(s, "view "+name+" {") + if i < 0 { + t.Fatalf("notation lacks view %s:\n%s", name, s) + } + j := strings.Index(s[i:], "\n }") + if j < 0 { + j = len(s) - i + } + return s[i : i+j] +} + +// A use case diagram that lists only an association's end still exposes the +// connection the association is drawn as and both usages it joins. +func TestUseCaseDiagramShowingAnAssociationEndExposesItsUsages(t *testing.T) { + r := migrateDocument(t, fmt.Sprintf(actorEndModel, "Ends", "SysML Use Case Diagram", "Use Case Diagram", "_eA"), "") + body := viewBody(t, r.Notation, "Ends") + for _, want := range []string{"expose 'Driver to Drive';", "expose Driver;", "expose Drive;"} { + if !strings.Contains(body, want) { + t.Errorf("view Ends lacks %q:\n%s", want, body) + } + } +} + +// Only a use case diagram exposes a connection's usages: a block definition +// diagram showing the same association exposes what it shows. +func TestTreeDiagramShowingAnAssociationExposesNoUsages(t *testing.T) { + r := migrateDocument(t, fmt.Sprintf(actorEndModel, "Blocks", "SysML Block Definition Diagram", "Class Diagram", + "_actor_uc_assoc"), "") + body := viewBody(t, r.Notation, "Blocks") + for _, want := range []string{"expose Driver;", "expose Drive;"} { + if !strings.Contains(body, want) { + t.Errorf("view Blocks lacks %q:\n%s", want, body) + } + } + for _, unwanted := range []string{"expose driver;", "expose drive;"} { + if strings.Contains(body, unwanted) { + t.Errorf("view Blocks exposes the usage %q:\n%s", unwanted, body) + } + } +} diff --git a/tests/migrate/stereotype_metadata_test.go b/tests/migrate/stereotype_metadata_test.go index 51285d2132..76586af956 100644 --- a/tests/migrate/stereotype_metadata_test.go +++ b/tests/migrate/stereotype_metadata_test.go @@ -56,6 +56,23 @@ func TestAllocateFromActionToPartIsAnAllocationDef(t *testing.T) { } } +// An «Allocate» whose end is a requirement, written as a usage no allocation +// end can be typed by, stays a dependency and the report says why. +func TestAllocateToARequirementStaysADependency(t *testing.T) { + r := migrateDocument(t, ` + + + `, ` + + + `) + wantLine(t, r.Notation, "requirement Flow;") + wantNoLine(t, r.Notation, "allocation def") + wantLine(t, r.Notation, "dependency Pump to Flow;") + wantNote(t, r, "_allocation", migrate.Approximated, "its end Flow has no enclosing written definition to type an allocation end, so a plain dependency stands for it") + wantClean(t, "allocate-to-requirement.sysml", r) +} + func TestAllocateFromDefinitionToFeatureKeepsTheFeature(t *testing.T) { members := ` diff --git a/tests/migrate/testdata/xmi/classifier_usages.golden.report.txt b/tests/migrate/testdata/xmi/classifier_usages.golden.report.txt new file mode 100644 index 0000000000..1f182f6f73 --- /dev/null +++ b/tests/migrate/testdata/xmi/classifier_usages.golden.report.txt @@ -0,0 +1,29 @@ +# SysML v1 to v2 migration report: classifier_usages.xmi +# exported by Example UML Tool +# migrated 22 element(s): 10 mapped, 12 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) + +## approximated (12) +Property Operations::Console::current _prop_current -> Operations::Console::current (a property typed by a behavior is written as a reference use case usage) +Actor Operations::Operator _act_operator -> Operations::Operator (a UML actor is written as a part usage) +Diagram Tables::Actors _diag_actors -> Tables::Actors (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +«DiagramTable» Diagram Tables::Classifiers _tbl_classifiers -> part def Tables::'Classifiers Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Classifiers Rows'; the views diagrams became and the action defs operations became are listed too) +Diagram Tables::Classifiers _diag_classifiers -> Tables::Classifiers (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +«DiagramTable» Diagram Tables::Namespaces _tbl_namespaces -> part def Tables::'Namespaces Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Namespaces Rows'; the views diagrams became are listed too; transitions and structured activity nodes are not) +Diagram Tables::Namespaces _diag_namespaces -> Tables::Namespaces (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +«DiagramTable» Diagram Tables::Packageable Elements _tbl_packageable -> part def Tables::'Packageable Elements Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Packageable Elements Rows'; the views diagrams became and the action defs operations became are listed too; instances of value types, written as attributes, are not) +Diagram Tables::Packageable Elements _diag_packageable -> Tables::'Packageable Elements' (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +«DiagramTable» Diagram Tables::Types _tbl_types -> part def Tables::'Types Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Types Rows'; the views diagrams became and the action defs operations became are listed too) +Diagram Tables::Types _diag_types -> Tables::Types (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Use Cases _diag_usecases -> Tables::'Use Cases' (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) + +## mapped (10) +Model Model _m (the root model's members are written at the top level) +Package Operations _pkg_operations -> Operations +Class Operations::Console _blk_console -> Operations::Console +Property Operations::Console::user _prop_user -> Operations::Console::user +UseCase Operations::Focus _uc_focus -> Operations::Focus +UseCase Operations::Scan _uc_scan -> Operations::Scan +Include Operations::Scan:: _inc_focus -> Operations::Scan +Package Tables _pkg_tables -> Tables +«DiagramTable» Diagram Tables::Actors _tbl_actors -> part def Tables::'Actors Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Actors Rows') +«DiagramTable» Diagram Tables::Use Cases _tbl_usecases -> part def Tables::'Use Cases Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Use Cases Rows') diff --git a/tests/migrate/testdata/xmi/classifier_usages.golden.sysml b/tests/migrate/testdata/xmi/classifier_usages.golden.sysml new file mode 100644 index 0000000000..d3d939a897 --- /dev/null +++ b/tests/migrate/testdata/xmi/classifier_usages.golden.sysml @@ -0,0 +1,184 @@ +package Operations { + part Operator; + use case Scan { + include Focus; + } + use case Focus; + occurrence def Console { + ref use case current :> Scan; + ref part user :> Operator; + } + connection 'Scan includes Focus' connect Scan to Focus; +} +package Tables { + view Classifiers { + expose 'Classifiers Document'; + render Views::asElementTable; + } + calc def 'Classifiers Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Union( + source = DocumentQueries::WhereType(source = candidates, type = ("Definition", "ViewUsage", "ViewpointUsage")), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = candidates))), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Classifiers Document' :> DocumentQueries::Document { + attribute redefines title = "Classifiers"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Classifiers"; + calc rows : 'Classifiers Rows'; + } + } + view Types { + expose 'Types Document'; + render Views::asElementTable; + } + calc def 'Types Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Union( + source = DocumentQueries::WhereType(source = candidates, type = ("Definition", "ViewUsage", "ViewpointUsage")), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = candidates))), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Types Document' :> DocumentQueries::Document { + attribute redefines title = "Types"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Types"; + calc rows : 'Types Rows'; + } + } + view Namespaces { + expose 'Namespaces Document'; + render Views::asElementTable; + } + calc def 'Namespaces Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Union( + source = DocumentQueries::WhereType(source = candidates, type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "StateUsage")), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = candidates))), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Namespaces Document' :> DocumentQueries::Document { + attribute redefines title = "Namespaces"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Namespaces"; + calc rows : 'Namespaces Rows'; + } + } + view 'Packageable Elements' { + expose 'Packageable Elements Document'; + render Views::asElementTable; + } + calc def 'Packageable Elements Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Union( + source = DocumentQueries::WhereType(source = candidates, type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "Dependency", "SatisfyRequirementUsage", "AllocationUsage")), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator", "Operations::Scan", "Operations::Focus")), + exclude = candidates))), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Packageable Elements Document' :> DocumentQueries::Document { + attribute redefines title = "Packageable Elements"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Packageable Elements"; + calc rows : 'Packageable Elements Rows'; + } + } + view Actors { + expose 'Actors Document'; + render Views::asElementTable; + } + calc def 'Actors Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Operator")), + exclude = candidates)), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Actors Document' :> DocumentQueries::Document { + attribute redefines title = "Actors"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Actors"; + calc rows : 'Actors Rows'; + } + } + view 'Use Cases' { + expose 'Use Cases Document'; + render Views::asElementTable; + } + calc def 'Use Cases Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))); + DocumentQueries::Project( + source = DocumentQueries::OrderBy( + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Scan", "Operations::Focus")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Operations::Scan", "Operations::Focus")), + exclude = candidates)), + property = "qualifiedName", + direction = "ascending", + missing = "last", + multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Use Cases Document' :> DocumentQueries::Document { + attribute redefines title = "Use Cases"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Use Cases"; + calc rows : 'Use Cases Rows'; + } + } +} diff --git a/tests/migrate/testdata/xmi/classifier_usages.xmi b/tests/migrate/testdata/xmi/classifier_usages.xmi new file mode 100644 index 0000000000..b2419cea5f --- /dev/null +++ b/tests/migrate/testdata/xmi/classifier_usages.xmi @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + diff --git a/tests/migrate/testdata/xmi/collectors.golden.sysml b/tests/migrate/testdata/xmi/collectors.golden.sysml index 90cc5d59c7..57a2f6ce3a 100644 --- a/tests/migrate/testdata/xmi/collectors.golden.sysml +++ b/tests/migrate/testdata/xmi/collectors.golden.sysml @@ -52,10 +52,10 @@ package Needs { doc /* What the yard * is asked to do. */ - requirement def <'Y-1'> Lift { + requirement <'Y-1'> Lift { doc /* The crane lifts a loaded container. */ } - requirement def <'Y-2'> Sway { + requirement <'Y-2'> Sway { doc /* The load sways less than one degree. */ comment /* Above the hook. */ view 'Sway Limits' { @@ -694,11 +694,14 @@ package 'Yard Handbook' { properties = ("name")) } calc def 'Yard Handbook Requirements Specification Table Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Needs"))); DocumentQueries::Project( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Needs"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Needs::Lift", "Needs::Sway")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Needs::Lift", "Needs::Sway")), + exclude = candidates)), properties = ("shortName", "name", "documentation")) } calc def 'Yard Handbook Documented Owners Rows' :> DocumentQueries::Query { @@ -719,7 +722,6 @@ package 'Yard Handbook' { part def 'Yard Handbook Document' :> DocumentQueries::Document { attribute redefines title = "Yard Handbook"; ref gantry : Structure::Gantry; - ref sway : Needs::Sway; ref crane : Structure::Crane; ref unnamed : Drafts::unnamed; ref sketchy : Drafts::Sketchy; @@ -760,7 +762,7 @@ package 'Yard Handbook' { attribute redefines title = "Kin Diagrams"; part diagram : DocumentQueries::Diagram { attribute redefines caption = "Sway Limits"; - ref redefines source = sway.'Sway Limits'; + ref redefines source = Needs::Sway.'Sway Limits'; } part 'diagram 2' : DocumentQueries::Diagram { attribute redefines caption = "Yard Requirements"; @@ -853,7 +855,7 @@ package 'Yard Handbook' { } part 'diagram 2' : DocumentQueries::Diagram { attribute redefines caption = "Sway Limits"; - ref redefines source = sway.'Sway Limits'; + ref redefines source = Needs::Sway.'Sway Limits'; } part 'diagram 3' : DocumentQueries::Diagram { attribute redefines caption = "Yard Requirements"; @@ -872,7 +874,7 @@ package 'Yard Handbook' { } part 'diagram 2' : DocumentQueries::Diagram { attribute redefines caption = "Sway Limits"; - ref redefines source = sway.'Sway Limits'; + ref redefines source = Needs::Sway.'Sway Limits'; } part 'diagram 3' : DocumentQueries::Diagram { attribute redefines caption = "Crane Internals"; diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt index 4e58721006..00139d0c3f 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt @@ -77,8 +77,8 @@ Property Structure::Vehicle::wheel _prop_wheel -> Structure::Vehicle::wheel «Block» Class Structure::Wheel _blk_wheel -> Structure::Wheel Package Usage _pkg_usage -> Usage UseCase Usage::Drive Vehicle _uc_drive -> Usage::'Drive Vehicle' -Include Usage::Drive Vehicle:: _inc_shown -> Usage::'Drive Vehicle'::'start Engine' -Include Usage::Drive Vehicle:: _inc_hidden -> Usage::'Drive Vehicle'::'stop Engine' -Diagram Usage::Driving Cases _diag_uc -> Usage::'Driving Cases' (a SysML Use Case Diagram written as a view rendered asTextualNotation) +Include Usage::Drive Vehicle:: _inc_shown -> Usage::'Drive Vehicle' +Include Usage::Drive Vehicle:: _inc_hidden -> Usage::'Drive Vehicle' +Diagram Usage::Driving Cases _diag_uc -> Usage::'Driving Cases' (a SysML Use Case Diagram written as a view rendered asInterconnectionDiagram) UseCase Usage::Start Engine _uc_start -> Usage::'Start Engine' UseCase Usage::Stop Engine _uc_stop -> Usage::'Stop Engine' diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml index 6402734d41..fdbeb9a05d 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml @@ -36,9 +36,9 @@ package Structure { binding 'set speed.value = target' bind 'set speed'.value = target; metadata MigrationMetadata::SynthesizedName about 'start to gain', final, 'gain.result to set speed.value', 'set speed.value = target'; } - satisfy requirement 'satisfy Mass Limit' : RequirementsModel::'Mass Limit'; - satisfy requirement : RequirementsModel::'Speed Limit'; - satisfy requirement 'satisfy Speed Limit' : RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Mass Limit' :> RequirementsModel::'Mass Limit'; + satisfy RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Speed Limit' :> RequirementsModel::'Speed Limit'; view 'Vehicle Internals' { expose engine; expose wheel; @@ -53,7 +53,7 @@ package Structure { metadata MigrationMetadata::SynthesizedName about 'engine to wheel', 'engine to wheel 2', 'mass = limit', 'satisfy Mass Limit', 'satisfy Speed Limit'; } part def Engine :> Component { - satisfy requirement 'satisfy Speed Limit' : RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Speed Limit' :> RequirementsModel::'Speed Limit'; metadata MigrationMetadata::SynthesizedName about 'satisfy Speed Limit'; } part def Wheel; @@ -101,15 +101,15 @@ package Behavior { } } package RequirementsModel { - requirement def 'Mass Limit' { + requirement 'Mass Limit' { doc /* The vehicle shall weigh less than 1500 kg. */ } - requirement def 'Speed Limit' { + requirement 'Speed Limit' { doc /* The vehicle shall not exceed 120 km/h. */ } verification def 'Mass Test' { objective 'objective' { - verify requirement 'verify Mass Limit' : 'Mass Limit'; + verify requirement 'verify Mass Limit' :> 'Mass Limit'; metadata MigrationMetadata::SynthesizedName about 'verify Mass Limit'; } metadata MigrationMetadata::SynthesizedName about 'objective'; @@ -126,18 +126,19 @@ package RequirementsModel { } } package Usage { - use case def 'Drive Vehicle' { - include use case 'start Engine' : 'Start Engine'; - include use case 'stop Engine' : 'Stop Engine'; - metadata MigrationMetadata::SynthesizedName about 'start Engine', 'stop Engine'; + use case 'Drive Vehicle' { + include 'Start Engine'; + include 'Stop Engine'; } - use case def 'Start Engine'; - use case def 'Stop Engine'; + use case 'Start Engine'; + use case 'Stop Engine'; + connection 'Drive Vehicle includes Start Engine' connect 'Drive Vehicle' to 'Start Engine'; + connection 'Drive Vehicle includes Stop Engine' connect 'Drive Vehicle' to 'Stop Engine'; view 'Driving Cases' { expose 'Drive Vehicle'; expose 'Start Engine'; - expose Usage::'Drive Vehicle'::'start Engine'; - render Views::asTextualNotation; + expose 'Drive Vehicle includes Start Engine'; + render Views::asInterconnectionDiagram; } } metadata MigrationMetadata::LibraryNameAvoided about RequirementsModel { sourceName = "Requirements"; } diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt index 869eb2e6b4..79560240bb 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt @@ -1,11 +1,11 @@ # SysML v1 to v2 migration report: diagram_edges.xmi # exported by Example UML Tool -# migrated 77 element(s): 68 mapped, 9 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 6 of 7 diagrams from diagram_edges.layout.xml: 19 elements positioned, 10 connectors routed, 0 styled, 0 notes +# migrated 77 element(s): 68 mapped, 9 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 6 of 7 diagrams from diagram_edges.layout.xml: 19 elements positioned, 11 connectors routed, 0 styled, 0 notes ## layout # source: diagram_edges.layout.xml (2022x v1.0.0, cameo 2022x Refresh2, exported 2025-06-12T21:38:50.779458500Z) # 6 diagram records: 6 joined, 0 matching no diagram; 0 views laid out from their own symbol stream, 1 without layout -# placements: 19 of 22 written (3 not exposed, 0 resolving to no element); routes: 10 of 22 written (11 not pinned, 1 resolving to no element); malformed: 0 +# placements: 19 of 22 written (3 not exposed, 0 resolving to no element); routes: 11 of 22 written (10 not pinned, 1 resolving to no element); malformed: 0 # styles: 0 of 0 written; notes: 0 written (0 anchored, 0 freed of an anchor the view does not lay out) # routes of BindingConnector: 1 written # routes of Connector: 1 unnamed @@ -14,7 +14,7 @@ # routes of ControlFlow: 2 written # routes of Dependency: 2 not drawn # routes of Generalization: 1 no v2 member -# routes of Include: 1 not drawn +# routes of Include: 1 written # routes of ObjectFlow: 1 not drawn # routes of ObjectFlow: 1 written # routes of Satisfy: 2 not drawn @@ -98,8 +98,8 @@ Property Structure::Vehicle::wheel _prop_wheel -> Structure::Vehicle::wheel «Block» Class Structure::Wheel _blk_wheel -> Structure::Wheel Package Usage _pkg_usage -> Usage UseCase Usage::Drive Vehicle _uc_drive -> Usage::'Drive Vehicle' -Include Usage::Drive Vehicle:: _inc_shown -> Usage::'Drive Vehicle'::'start Engine' -Include Usage::Drive Vehicle:: _inc_hidden -> Usage::'Drive Vehicle'::'stop Engine' -Diagram Usage::Driving Cases _diag_uc -> Usage::'Driving Cases' (a SysML Use Case Diagram written as a view rendered asTextualNotation; laid out from diagram_edges.layout.xml: 2 of 2 shown elements positioned, 0 of 1 connectors routed (1 not drawn)) +Include Usage::Drive Vehicle:: _inc_shown -> Usage::'Drive Vehicle' +Include Usage::Drive Vehicle:: _inc_hidden -> Usage::'Drive Vehicle' +Diagram Usage::Driving Cases _diag_uc -> Usage::'Driving Cases' (a SysML Use Case Diagram written as a view rendered asInterconnectionDiagram; laid out from diagram_edges.layout.xml: 2 of 2 shown elements positioned, 1 of 1 connectors routed) UseCase Usage::Start Engine _uc_start -> Usage::'Start Engine' UseCase Usage::Stop Engine _uc_stop -> Usage::'Stop Engine' diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml index 933da07501..9e436a3ff5 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml @@ -43,9 +43,9 @@ package Structure { binding 'set speed.value = target' bind 'set speed'.value = target; metadata MigrationMetadata::SynthesizedName about 'start to gain', final, 'gain.result to set speed.value', 'set speed.value = target'; } - satisfy requirement 'satisfy Mass Limit' : RequirementsModel::'Mass Limit'; - satisfy requirement : RequirementsModel::'Speed Limit'; - satisfy requirement 'satisfy Speed Limit' : RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Mass Limit' :> RequirementsModel::'Mass Limit'; + satisfy RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Speed Limit' :> RequirementsModel::'Speed Limit'; view 'Vehicle Internals' { expose engine; expose wheel; @@ -69,7 +69,7 @@ package Structure { metadata MigrationMetadata::SynthesizedName about 'engine to wheel', 'engine to wheel 2', 'mass = limit', 'satisfy Mass Limit', 'satisfy Speed Limit'; } part def Engine :> Component { - satisfy requirement 'satisfy Speed Limit' : RequirementsModel::'Speed Limit'; + satisfy requirement 'satisfy Speed Limit' :> RequirementsModel::'Speed Limit'; metadata MigrationMetadata::SynthesizedName about 'satisfy Speed Limit'; } part def Wheel; @@ -129,15 +129,15 @@ package Behavior { } } package RequirementsModel { - requirement def 'Mass Limit' { + requirement 'Mass Limit' { doc /* The vehicle shall weigh less than 1500 kg. */ } - requirement def 'Speed Limit' { + requirement 'Speed Limit' { doc /* The vehicle shall not exceed 120 km/h. */ } verification def 'Mass Test' { objective 'objective' { - verify requirement 'verify Mass Limit' : 'Mass Limit'; + verify requirement 'verify Mass Limit' :> 'Mass Limit'; metadata MigrationMetadata::SynthesizedName about 'verify Mass Limit'; } metadata MigrationMetadata::SynthesizedName about 'objective'; @@ -158,21 +158,23 @@ package RequirementsModel { } } package Usage { - use case def 'Drive Vehicle' { - include use case 'start Engine' : 'Start Engine'; - include use case 'stop Engine' : 'Stop Engine'; - metadata MigrationMetadata::SynthesizedName about 'start Engine', 'stop Engine'; + use case 'Drive Vehicle' { + include 'Start Engine'; + include 'Stop Engine'; } - use case def 'Start Engine'; - use case def 'Stop Engine'; + use case 'Start Engine'; + use case 'Stop Engine'; + connection 'Drive Vehicle includes Start Engine' connect 'Drive Vehicle' to 'Start Engine'; + connection 'Drive Vehicle includes Stop Engine' connect 'Drive Vehicle' to 'Stop Engine'; view 'Driving Cases' { expose 'Drive Vehicle'; expose 'Start Engine'; - expose Usage::'Drive Vehicle'::'start Engine'; + expose 'Drive Vehicle includes Start Engine'; @DiagramLayout::Canvas { unit = "px"; width = 300; height = 50; } metadata DiagramLayout::Layout about 'Drive Vehicle' { x = 10; y = 10; width = 100; height = 40; } metadata DiagramLayout::Layout about 'Start Engine' { x = 200; y = 10; width = 100; height = 40; } - render Views::asTextualNotation; + metadata DiagramLayout::Route about 'Drive Vehicle includes Start Engine' { points = (200, 30, 110, 30); } + render Views::asInterconnectionDiagram; } } metadata MigrationMetadata::LibraryNameAvoided about RequirementsModel { sourceName = "Requirements"; } diff --git a/tests/migrate/testdata/xmi/diagrams.golden.report.txt b/tests/migrate/testdata/xmi/diagrams.golden.report.txt index 988de7fab3..c245d1fd57 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagrams.golden.report.txt @@ -8,17 +8,17 @@ Diagram Behavior::Drive::start engine _diag_act_node -> Behavior::Drive::'start Diagram Behavior::Modes::::Idle _diag_stm -> Behavior::Modes::'Idle 2' (a SysML State Machine Diagram written as a view rendered asInterconnectionDiagram of StateTransitionView; the view exposes state def Behavior::Modes, whose graph the rendering draws with the 3 shown nodes and edges of it; its owner Region Behavior::Modes:: has no v2 body; written in state def Behavior::Modes; written as Idle 2 since a member of its owner is also named Idle) «Expose» Dependency Reports:: _expose_fleet (written as 2 relationships, one per client–supplier pair) Diagram Reports::Fleet _diag_fleet -> Reports::'Fleet 2' (a SysML Package Diagram written as a view rendered asTreeDiagram; written as Fleet 2 since a member of its owner is also named Fleet) -Diagram Structure::Engine _diag_custom -> Structure::'Engine 2' (a Team Review Diagram written as a view rendered asTextualNotation; written as Engine 2 since a member of its owner is also named Engine) +Diagram Structure::Engine _diag_custom -> Structure::'Engine 2' (a Team Review Diagram written as a view rendered asInterconnectionDiagram; written as Engine 2 since a member of its owner is also named Engine) OpaqueAction Structure::Engine::Cranking::ignite _crank_ignite -> ignite (the JavaScript body is translated to v2; ignited is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it) Diagram Structure::Engine::Cranking::ignite::Ignition _diag_ignite -> Structure::Engine::Crank::Ignition (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Structure::Engine::Crank, whose graph the rendering draws with the 2 shown nodes and edges of it; its owner OpaqueAction Structure::Engine::Cranking::ignite has no v2 body; written in action def Structure::Engine::Crank) Operation Structure::Engine::Halt _op_halt -> Structure::Engine::Halt (the JavaScript body is translated to v2; ignited is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it; its owner's usage halt performs it, as a call on an object does) Diagram Structure::Vehicle Structure _diag_bdd -> Structure::'Vehicle Structure' (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; 2 of 10 shown elements are not written and not exposed) Diagram Structure::Vehicle Structure _diag_pkg -> Structure::'Vehicle Structure 2' (a SysML Package Diagram written as a view rendered asTreeDiagram; written as Vehicle Structure 2 since a member of its owner is also named Vehicle Structure) Diagram Structure::Vehicle::engine::Engine Matrix _diag_matrix -> Structure::Vehicle::'Engine Matrix' (a Dependency Matrix written as a view rendered asElementTable; its owner Property Structure::Vehicle::engine has no v2 body; written in part def Structure::Vehicle) -Property Usage:::: _end_uc -> Usage::'Drive Vehicle'::driver (the end at the use case is the use case def the actor is declared in) +Property Usage:::: _end_uc -> Usage::'Driver to Drive Vehicle' (the end at the use case is the connection's end at the use case usage) +Property Usage::::driver _end_driver -> Usage::'Driver to Drive Vehicle' (the end at the actor is the connection's end at the actor's part usage) Comment Usage:: _cmt_drives (the comment also annotates (_assoc_driver), which has no v2 declaration in the document and is not written as a subject) -Actor Usage::Driver _actor_driver -> Usage::Driver (a UML actor is written as a part def) -Diagram Usage::Driving _diag_usage -> Usage::Driving (a SysML Use Case Diagram written as a view rendered asTextualNotation; 1 of 5 shown elements are not written and not exposed) +Actor Usage::Driver _actor_driver -> Usage::Driver (a UML actor is written as a part usage) ## mapped (46) Package Behavior _pkg_behavior -> Behavior @@ -64,9 +64,9 @@ Property Structure::Vehicle::mass _prop_mass -> Structure::Vehicle::mass Property Structure::Vehicle::wheel _prop_wheel -> Structure::Vehicle::wheel «Block» Class Structure::Wheel _blk_wheel -> Structure::Wheel Package Usage _pkg_usage -> Usage -Association Usage:: _assoc_driver -> Usage::'Drive Vehicle'::driver (the anonymous association to the actor is written as an actor of the use case) -Property Usage::::driver _end_driver -> Usage::'Drive Vehicle'::driver -UseCase Usage::Drive Vehicle _uc_drive -> Usage::'Drive Vehicle' (v2 places the subject before the actors, so an anonymous subject is declared) +Association Usage:: _assoc_driver -> Usage::'Driver to Drive Vehicle' (the anonymous association to the actor is written as a connection between the actor and the use case) +UseCase Usage::Drive Vehicle _uc_drive -> Usage::'Drive Vehicle' +Diagram Usage::Driving _diag_usage -> Usage::Driving (a SysML Use Case Diagram written as a view rendered asInterconnectionDiagram) ## skipped (2) Comment _note_bdd (tool-private xmi:Extension content written by Example UML Tool 1.0) diff --git a/tests/migrate/testdata/xmi/diagrams.golden.sysml b/tests/migrate/testdata/xmi/diagrams.golden.sysml index 6e1b0e3c26..b510309b8c 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.sysml +++ b/tests/migrate/testdata/xmi/diagrams.golden.sysml @@ -77,7 +77,7 @@ package Structure { } view 'Engine 2' { expose Engine; - render Views::asTextualNotation; + render Views::asInterconnectionDiagram; } } package Behavior { @@ -113,16 +113,14 @@ package Behavior { } package Usage { comment /* The driver drives. */ - part def Driver; - use case def 'Drive Vehicle' { - subject; - actor driver : Driver; - } + part Driver; + use case 'Drive Vehicle'; + connection 'Driver to Drive Vehicle' connect Driver to 'Drive Vehicle'; view Driving { expose Driver; expose 'Drive Vehicle'; - expose 'Drive Vehicle'::driver; - render Views::asTextualNotation; + expose 'Driver to Drive Vehicle'; + render Views::asInterconnectionDiagram; } } package Reports { diff --git a/tests/migrate/testdata/xmi/docgen_columns.golden.sysml b/tests/migrate/testdata/xmi/docgen_columns.golden.sysml index e620a8bc00..77d2e8f45c 100644 --- a/tests/migrate/testdata/xmi/docgen_columns.golden.sysml +++ b/tests/migrate/testdata/xmi/docgen_columns.golden.sysml @@ -254,10 +254,10 @@ package Observatory { DocumentQueries::Project( source = DocumentQueries::OrderBy( source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), exclude = DocumentQueries::Except( source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), property = "name", direction = "ascending", missing = "last", @@ -273,10 +273,10 @@ package Observatory { DocumentQueries::Project( source = DocumentQueries::OrderBy( source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), exclude = DocumentQueries::Except( source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), property = "name", direction = "ascending", missing = "last", diff --git a/tests/migrate/testdata/xmi/documents.golden.sysml b/tests/migrate/testdata/xmi/documents.golden.sysml index 9c52783e5b..b4caeb6633 100644 --- a/tests/migrate/testdata/xmi/documents.golden.sysml +++ b/tests/migrate/testdata/xmi/documents.golden.sysml @@ -11,7 +11,7 @@ package Fleet { part axles : Axle[2] { attribute mass : ScalarValues::Real; } - satisfy requirement : Fleet::Requirements::'Load Limit'; + satisfy Fleet::Requirements::'Load Limit'; view 'Truck Internals' { expose axles; render Views::asInterconnectionDiagram; @@ -20,7 +20,7 @@ package Fleet { part def Trailer { doc /* Carries the load. */ attribute payload : ScalarValues::Real; - satisfy requirement : Fleet::Requirements::'Brake Distance'; + satisfy Fleet::Requirements::'Brake Distance'; } part def Axle; view 'Truck Structure' { @@ -31,18 +31,18 @@ package Fleet { } } package Requirements { - requirement def <'R-1'> 'Load Limit' { + requirement <'R-1'> 'Load Limit' { doc /* The payload stays under the axle rating. */ comment /* The payload stays under the axle rating. */ } - requirement def <'R-2'> 'Brake Distance' { + requirement <'R-2'> 'Brake Distance' { doc /* A loaded truck stops within the legal distance. */ comment /* A loaded truck stops within the legal distance. */ @'Fleet Profile'::Safety { level = "brakes"; } } - requirement def <'R-3'> 'Axle Count' { + requirement <'R-3'> 'Axle Count' { doc /* A truck has two axles. */ @'Fleet Profile'::Safety { level = "many"; @@ -434,11 +434,17 @@ package 'Fleet Documents' { maxDepth = 1); DocumentQueries::Project( source = DocumentQueries::OrderBy( - source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + source = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = candidates))), property = "name", direction = "ascending", missing = "last", @@ -451,23 +457,29 @@ package 'Fleet Documents' { DocumentQueries::Column(name = "Axle Mass", expression = Fleet::Structure::Truck::axles.mass ?? ""))) } calc def 'Fleet Handbook Requirement List Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements"))); DocumentQueries::Project( source = DocumentQueries::WhereName( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = candidates)), operator = "matches", value = "^(?:Axle.*)$|^(?:Brake.*)$|^(?:Load.*)$"), properties = ("name")) } calc def 'Fleet Handbook Requirement Texts Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements"))); DocumentQueries::Project( source = DocumentQueries::WhereName( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Fleet::Requirements::Load Limit", "Fleet::Requirements::Brake Distance", "Fleet::Requirements::Axle Count")), + exclude = candidates)), operator = "matches", value = "^(?:Axle.*)$|^(?:Brake.*)$|^(?:Load.*)$"), properties = ("documentation")) diff --git a/tests/migrate/testdata/xmi/empty_classifier_tables.golden.report.txt b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.report.txt new file mode 100644 index 0000000000..31480b7e79 --- /dev/null +++ b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.report.txt @@ -0,0 +1,19 @@ +# SysML v1 to v2 migration report: empty_classifier_tables.xmi +# exported by Example UML Tool +# migrated 12 element(s): 6 mapped, 6 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) + +## approximated (6) +Diagram Tables::Actors _diag_actors -> Tables::Actors (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Classifiers _diag_classifiers -> Tables::Classifiers (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Namespaces _diag_namespaces -> Tables::Namespaces (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Packageable Elements _diag_packageable -> Tables::'Packageable Elements' (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Types _diag_types -> Tables::Types (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) +Diagram Tables::Use Cases _diag_usecases -> Tables::'Use Cases' (a Generic Table written as a view rendered asElementTable; the diagram shows nothing; the view exposes nothing) + +## mapped (6) +Model Model _m (the root model's members are written at the top level) +Package Operations _pkg_operations -> Operations +Class Operations::Console _blk_console -> Operations::Console +Package Tables _pkg_tables -> Tables +«DiagramTable» Diagram Tables::Actors _tbl_actors -> part def Tables::'Actors Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Actors Rows') +«DiagramTable» Diagram Tables::Use Cases _tbl_usecases -> part def Tables::'Use Cases Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Use Cases Rows') diff --git a/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml new file mode 100644 index 0000000000..6faf2e2e32 --- /dev/null +++ b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml @@ -0,0 +1,49 @@ +package Operations { + occurrence def Console; +} +package Tables { + view Classifiers { + render Views::asElementTable; + } + view Types { + render Views::asElementTable; + } + view Namespaces { + render Views::asElementTable; + } + view 'Packageable Elements' { + render Views::asElementTable; + } + view Actors { + expose 'Actors Document'; + render Views::asElementTable; + } + calc def 'Actors Rows' :> DocumentQueries::Query { + DocumentQueries::Project( + source = DocumentQueries::OrderBy(source = (), property = "qualifiedName", direction = "ascending", missing = "last", multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Actors Document' :> DocumentQueries::Document { + attribute redefines title = "Actors"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Actors"; + calc rows : 'Actors Rows'; + } + } + view 'Use Cases' { + expose 'Use Cases Document'; + render Views::asElementTable; + } + calc def 'Use Cases Rows' :> DocumentQueries::Query { + DocumentQueries::Project( + source = DocumentQueries::OrderBy(source = (), property = "qualifiedName", direction = "ascending", missing = "last", multiple = "first"), + properties = ("qualifiedName")) + } + part def 'Use Cases Document' :> DocumentQueries::Document { + attribute redefines title = "Use Cases"; + part rows : DocumentQueries::Table { + attribute redefines caption = "Use Cases"; + calc rows : 'Use Cases Rows'; + } + } +} diff --git a/tests/migrate/testdata/xmi/empty_classifier_tables.xmi b/tests/migrate/testdata/xmi/empty_classifier_tables.xmi new file mode 100644 index 0000000000..c6ccda5747 --- /dev/null +++ b/tests/migrate/testdata/xmi/empty_classifier_tables.xmi @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + + + QPROP:Element:qualifiedName + QPROP:Element:qualifiedName^Asc + + diff --git a/tests/migrate/testdata/xmi/org_profile.golden.sysml b/tests/migrate/testdata/xmi/org_profile.golden.sysml index 5698519c88..0b09a0b80b 100644 --- a/tests/migrate/testdata/xmi/org_profile.golden.sysml +++ b/tests/migrate/testdata/xmi/org_profile.golden.sysml @@ -46,7 +46,7 @@ package 'Pump System' { precision = 3; } } - requirement def <'REQ-1'> 'Flow Requirement' { + requirement <'REQ-1'> 'Flow Requirement' { doc /* The pump shall deliver 10 l/s. */ @'Org Profile'::'Org Requirement' { Rationale = "Sized for the peak load."; @@ -55,7 +55,7 @@ package 'Pump System' { weight = 2.0; } } - requirement def <'REQ-2'> 'Safety Requirement' { + requirement <'REQ-2'> 'Safety Requirement' { doc /* The pump shall stop on overpressure. */ @'Org Profile'::'Key Requirement' { criticality = 'Org Profile'::Criticality::low; @@ -80,7 +80,7 @@ package 'Pump System' { reviewer = "EE"; } } - satisfy requirement : 'Flow Requirement' { + satisfy 'Flow Requirement' { @'Org Profile'::'Org Satisfy' { verified = true; } diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt index dab7b55b9a..1d3c9c1dc8 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt @@ -1,26 +1,30 @@ # SysML v1 to v2 migration report: parking_usecases.xmi -# migrated 44 element(s): 29 mapped, 12 approximated, 3 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 44 element(s): 25 mapped, 16 approximated, 3 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## unmapped (3) Extend Use Cases::Leave Garage:: _extLost (the extended use case is not in the document; 1 extendedCase reference(s) resolve to nothing in the document (_lostCase)) Include Use Cases::Leave Garage:: _incLost (the included use case is not in the document; 1 addition reference(s) resolve to nothing in the document (_lostCase)) -ExtensionPoint Use Cases::Park Car::garage full _epFull (v2 has no extension points; an extending use case is written as a dependency on the extended one) +ExtensionPoint Use Cases::Park Car::garage full _epFull (v2 has no extension points; an extending use case is written as a connection to the extended one) -## approximated (12) -Actor Context::Attendant _attendant -> Context::Attendant (a UML actor is written as a part def) -Actor Context::Bank _bank -> Context::Bank (a UML actor is written as a part def) -Actor Context::Driver _driver -> Context::Driver (a UML actor is written as a part def) +## approximated (16) +Actor Context::Attendant _attendant -> Context::Attendant (a UML actor is written as a part usage) +Actor Context::Bank _bank -> Context::Bank (a UML actor is written as a part usage) +Actor Context::Driver _driver -> Context::Driver (a UML actor is written as a part usage) Association Use Cases:: _aParkWait -> 'Use Cases'::unnamed (the anonymous Association owns every end, so it is written as connection def unnamed) -Property Use Cases:::: _eParkD -> 'Use Cases'::'Park Car'::driver (the end at the use case is the use case def the actor is declared in) -Property Use Cases:::: _eParkA -> 'Use Cases'::'Park Car'::attendant (the end at the use case is the use case def the actor is declared in) +Property Use Cases:::: _eParkD -> 'Driver to Park Car' (the end at the use case is the connection's end at the use case usage) +Property Use Cases:::: _eAttendant -> 'Attendant to Park Car' (the end at the actor is the connection's end at the actor's part usage) +Property Use Cases:::: _eParkA -> 'Attendant to Park Car' (the end at the use case is the connection's end at the use case usage) +Property Use Cases::::driver _eDriver -> 'Driver to Park Car' (the end at the actor is the connection's end at the actor's part usage) Activity Use Cases::Park Car::Park Flow _parkFlow -> 'Use Cases'::'Park Car'::'Park Flow' (the classifier behavior is run by every object of Use Cases::Park Car as its usage park Flow) OpaqueAction Use Cases::Park Car::Park Flow::Enter _pfEnter -> Enter (the body is kept as a comment: the body is empty) UseCase Use Cases::Pay Fee _pay -> 'Use Cases'::'Pay Fee' (v2 admits one subject per case, so the subject Context::Ticket is written as the reference usage ticket) -Property Use Cases::Settlement:: _ePayB -> 'Use Cases'::'Pay Fee'::bank (the end at the use case is the use case def the actor is declared in) +Include Use Cases::Pay Fee::authorise _incAuth -> 'Use Cases'::'Pay Fee' (an include referring to a use case usage has no name, so authorise is dropped) +Property Use Cases::Settlement:: _ePayB -> settlement (the end at the use case is the connection's end at the use case usage) +Property Use Cases::Settlement::bank _eBank -> settlement (the end at the actor is the connection's end at the actor's part usage) Property Use Cases::Valet Service::offer _svcCase -> 'Use Cases'::'Valet Service'::offer (a property typed by a behavior is written as a reference use case usage) -Extend Use Cases::Wait For Space:: _extFull (v2 has no extend: the extension is written as a plain dependency on the extended use case; it applies at extension point(s) 'garage full' when {English} no bay is free, which only a comment keeps) +Extend Use Cases::Wait For Space:: _extFull -> 'Use Cases'::'Wait For Space extends Park Car' (v2 has no extend: the extension is written as a connection between usages of the extending use case and the extended use case; it applies at extension point(s) 'garage full' when {English} no bay is free, which only a comment keeps) -## mapped (29) +## mapped (25) Package Context _ctx -> Context Comment Context::Driver:: _driverDoc «Block» Class Context::Garage _garage -> Context::Garage @@ -28,25 +32,21 @@ Comment Context::Driver:: _driverDoc Model Model _m (the root model's members are written at the top level) PrimitiveType Real _real -> Real Package Use Cases _ucs -> 'Use Cases' -Association Use Cases:: _aDriverPark -> 'Use Cases'::'Park Car'::driver (the anonymous association to the actor is written as an actor of the use case) -Association Use Cases:: _aAttendantPark -> 'Use Cases'::'Park Car'::attendant (the anonymous association to the actor is written as an actor of the use case) -Property Use Cases:::: _eAttendant -> 'Use Cases'::'Park Car'::attendant +Association Use Cases:: _aDriverPark -> 'Driver to Park Car' (the anonymous association to the actor is written as a connection between the actor and the use case) +Association Use Cases:: _aAttendantPark -> 'Attendant to Park Car' (the anonymous association to the actor is written as a connection between the actor and the use case) Property Use Cases:::: _ePW1 -> 'Use Cases'::unnamed::'park Car' Property Use Cases:::: _ePW2 -> 'Use Cases'::unnamed::'wait For Space' -Property Use Cases::::driver _eDriver -> 'Use Cases'::'Park Car'::driver UseCase Use Cases::Authorise Payment _authorise -> 'Use Cases'::'Authorise Payment' (v2 places the subject before the actors, so an anonymous subject is declared) Property Use Cases::Authorise Payment::bank _authBank -> 'Use Cases'::'Authorise Payment'::bank «HyperlinkOwner» UseCase Use Cases::Leave Garage _leave -> 'Use Cases'::'Leave Garage' («HyperlinkOwner» from http://www.example.org/profiles/Custom is applied from a profile the document does not define; the application is kept as a comment) UseCase Use Cases::Park Car _park -> 'Use Cases'::'Park Car' Comment Use Cases::Park Car:: _parkDoc -Include Use Cases::Park Car:: _incPay -> 'Use Cases'::'Park Car'::'pay Fee' +Include Use Cases::Park Car:: _incPay -> 'Use Cases'::'Park Car' ActivityFinalNode Use Cases::Park Car::Park Flow:: _pfEnd -> final ControlFlow Use Cases::Park Car::Park Flow:: _pfE2 InitialNode Use Cases::Park Car::Park Flow:: _pfStart -> start Property Use Cases::Pay Fee::amount _payAmount -> 'Use Cases'::'Pay Fee'::amount -Include Use Cases::Pay Fee::authorise _incAuth -> 'Use Cases'::'Pay Fee'::authorise -Association Use Cases::Settlement _aBankPay -> 'Use Cases'::Settlement (the association is also written as the actor bank of the use case 'Use Cases'::'Pay Fee') -Property Use Cases::Settlement::bank _eBank -> 'Use Cases'::'Pay Fee'::bank +Association Use Cases::Settlement _aBankPay -> 'Use Cases'::Settlement (the association's one usage is the connection settlement joining the actor and the use case 'Use Cases'::'Pay Fee') UseCase Use Cases::Valet Park _valet -> 'Use Cases'::'Valet Park' «Block» Class Use Cases::Valet Service _service -> 'Use Cases'::'Valet Service' UseCase Use Cases::Wait For Space _wait -> 'Use Cases'::'Wait For Space' diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml index fdb21ebbbd..560c732b33 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml @@ -1,18 +1,18 @@ package Context { part def Garage; part def Ticket; - part def Driver { + part Driver { doc /* The person parking a car. */ } - part def Attendant; - part def Bank; + part Attendant; + part Bank; } package 'Use Cases' { - use case def 'Park Car' { + use case 'Park Car' { doc /* A driver parks a car in the garage. */ subject garage : Context::Garage; - include use case 'pay Fee' : 'Pay Fee'; - /* not migrated: ExtensionPoint 'garage full' — v2 has no extension points; an extending use case is written as a dependency on the extended one */ + include 'Pay Fee'; + /* not migrated: ExtensionPoint 'garage full' — v2 has no extension points; an extending use case is written as a connection to the extended one */ action def 'Park Flow' { first start then Enter; action Enter { @@ -22,45 +22,50 @@ package 'Use Cases' { action final terminate; metadata MigrationMetadata::SynthesizedName about final; } - actor driver : Context::Driver; - actor attendant : Context::Attendant[0..1]; perform action 'park Flow' : 'Park Flow'; - metadata MigrationMetadata::SynthesizedName about garage, 'pay Fee'; + metadata MigrationMetadata::SynthesizedName about garage; } - use case def 'Pay Fee' { + use case 'Pay Fee' { subject garage : Context::Garage; ref part ticket : Context::Ticket; - include use case authorise : 'Authorise Payment'; + include 'Authorise Payment'; attribute amount : Real; - actor bank : Context::Bank; metadata MigrationMetadata::SynthesizedName about garage, ticket; } - use case def 'Authorise Payment' { + use case 'Authorise Payment' { subject; - actor bank : Context::Bank; + actor bank :> Context::Bank; } - use case def 'Wait For Space' { - dependency 'Wait For Space' to 'Park Car'; /* extends at extension point(s) 'garage full' when {English} no bay is free */ - } - use case def 'Leave Garage' { + use case 'Wait For Space'; + use case 'Leave Garage' { /* not migrated: Include (_incLost) — the included use case is not in the document; 1 addition reference(s) resolve to nothing in the document (_lostCase) */ /* not migrated: Extend (_extLost) — the extended use case is not in the document; 1 extendedCase reference(s) resolve to nothing in the document (_lostCase) */ /* applied stereotype «HyperlinkOwner» */ } - use case def 'Valet Park' :> 'Park Car'; + use case 'Valet Park' :> 'Park Car'; part def 'Valet Service' { - ref use case offer : 'Valet Park'; + ref use case offer :> 'Valet Park'; } connection def Settlement { - end bank : Context::Bank; - end 'pay Fee' : 'Pay Fee'; + end bank :> Context::Bank; + end 'pay Fee' :> 'Pay Fee'; metadata MigrationMetadata::SynthesizedName about 'pay Fee'; } connection def unnamed { - end 'park Car' : 'Park Car'; - end 'wait For Space' : 'Wait For Space'; + end 'park Car' :> 'Park Car'; + end 'wait For Space' :> 'Wait For Space'; metadata MigrationMetadata::SynthesizedName about 'park Car', 'wait For Space'; } + connection 'Park Car includes Pay Fee' connect 'Park Car' to 'Pay Fee'; + connection authorise connect 'Pay Fee' to 'Authorise Payment' { + doc /* Pay Fee includes Authorise Payment */ + } + connection 'Wait For Space extends Park Car' connect 'Wait For Space' to 'Park Car' { + doc /* extends at extension point(s) 'garage full' when {English} no bay is free */ + } metadata MigrationMetadata::SynthesizedName about unnamed; } attribute def Real; +connection 'Driver to Park Car' connect Context::Driver to 'Use Cases'::'Park Car'; +connection 'Attendant to Park Car' connect Context::Attendant to 'Use Cases'::'Park Car'; +connection settlement : 'Use Cases'::Settlement connect Context::Bank to 'Use Cases'::'Pay Fee'; diff --git a/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt b/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt index 860c9c5156..19e68b95d2 100644 --- a/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt +++ b/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt @@ -1,19 +1,19 @@ # SysML v1 to v2 migration report: profile_inheritance.xmi # exported by Example UML Tool -# migrated 36 element(s): 31 mapped, 5 approximated, 0 unmapped (4 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 36 element(s): 30 mapped, 6 approximated, 0 unmapped (4 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (5) +## approximated (6) +«Detailed By» Abstraction System:: _refine -> System::'Spin refines Speed Requirement' (written as a connection between usages of its ends, which a diagram draws; the standard Refinement metadata annotates only a dependency, so the connection's name says it refines) «Linked To» Abstraction System:: _trace (a trace is written as a plain dependency) «TestCase» Activity System::Torque Test::run _test_act -> System::'Torque Test'::run (the test case's behavior is not migrated; only its verified requirements are) Stereotype Tailoring::Foreign _st_foreign -> Tailoring::Foreign (generalization of http://example.com/projects/Shared_Profile.xmi#_shared_req is not written: it is not a user stereotype defined in the document) Stereotype Tailoring::Lost _st_lost -> Tailoring::Lost (a generalization refers to nothing in the document) Stereotype Tailoring::Ring B _st_ring_b -> Tailoring::'Ring B' (generalization of Tailoring::Ring A is not written: it closes a cycle of generalizations) -## mapped (31) +## mapped (30) Model Model _m (the root model's members are written at the top level) Package System _sys -> System «Checked By» Abstraction System:: _verify -> System::'Torque Test'::run -«Detailed By» Abstraction System:: _refine «Derived From» Abstraction System:: _derive -> System::'Derive Speed Requirement' «Assigned To» Abstraction System:: _alloc -> System::'Spin to Shaft' «Foreign» Class System::Foreign Thing _foreign -> System::'Foreign Thing' diff --git a/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml index 52926b0e52..3466708473 100644 --- a/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml +++ b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml @@ -33,19 +33,19 @@ package System { side = 1; } } - requirement def <'R-1'> 'Torque Requirement' { + requirement <'R-1'> 'Torque Requirement' { doc /* Torque shall exceed 10 Nm. */ @Tailoring::'Audited Requirement' { auditor = "QA"; } } - requirement def <'R-2'> 'Speed Requirement' { + requirement <'R-2'> 'Speed Requirement' { doc /* Speed shall exceed 100 rpm. */ } part def 'Torque Test' { verification def run { objective { - verify requirement : 'Torque Requirement' { + verify 'Torque Requirement' { @Tailoring::'Checked By'; } } @@ -61,17 +61,13 @@ package System { occurrence def 'Foreign Thing' { @Tailoring::Foreign; } - dependency Spin to 'Speed Requirement' { - @ModelingMetadata::Refinement; - @Tailoring::'Detailed By'; - } dependency 'Speed Requirement' to Gear { /* «Trace» */ @Tailoring::'Linked To'; } connection def 'Derive Speed Requirement' :> RequirementDerivation::Derivation { - end #RequirementDerivation::original originalRequirement : 'Torque Requirement'; - end #RequirementDerivation::derive derivedRequirement : 'Speed Requirement'; + end #RequirementDerivation::original originalRequirement :> 'Torque Requirement'; + end #RequirementDerivation::derive derivedRequirement :> 'Speed Requirement'; @Tailoring::'Derived From'; } allocation def 'Spin to Shaft' { @@ -79,5 +75,9 @@ package System { end :>> target : Shaft; @Tailoring::'Assigned To'; } + action spin : Spin; + connection 'Spin refines Speed Requirement' connect spin to 'Speed Requirement' { + @Tailoring::'Detailed By'; + } metadata MigrationMetadata::SynthesizedName about 'Spin to Shaft'; } diff --git a/tests/migrate/testdata/xmi/relation_subtypes.golden.sysml b/tests/migrate/testdata/xmi/relation_subtypes.golden.sysml index ad4d25b226..617b0fa4b6 100644 --- a/tests/migrate/testdata/xmi/relation_subtypes.golden.sysml +++ b/tests/migrate/testdata/xmi/relation_subtypes.golden.sysml @@ -4,10 +4,10 @@ package Traceability { package Plant { package Structure { part def Pump { - satisfy requirement : Plant::Requirements::FlowRequirement; + satisfy Plant::Requirements::FlowRequirement; } part def Valve { - satisfy requirement : Plant::Requirements::SealRequirement { + satisfy Plant::Requirements::SealRequirement { @Traceability::Fulfil; } } @@ -23,11 +23,17 @@ package Plant { direction = "outgoing", maxDepth = 1); DocumentQueries::Project( - source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + source = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates))), properties = ("qualifiedName", "@type")) } part def 'Valve Requirement Map Document' :> DocumentQueries::Document { @@ -39,15 +45,15 @@ package Plant { } } package Requirements { - requirement def <'R-1'> FlowRequirement { + requirement <'R-1'> FlowRequirement { doc /* The pump keeps the flow above the minimum. */ } - requirement def <'R-2'> SealRequirement { + requirement <'R-2'> SealRequirement { doc /* Every valve seals when closed. */ } connection def 'Derive SealRequirement' :> RequirementDerivation::Derivation { - end #RequirementDerivation::original originalRequirement : FlowRequirement; - end #RequirementDerivation::derive derivedRequirement : SealRequirement; + end #RequirementDerivation::original originalRequirement :> FlowRequirement; + end #RequirementDerivation::derive derivedRequirement :> SealRequirement; } view 'Exact Satisfaction Matrix' { expose FlowRequirement; @@ -57,12 +63,15 @@ package Plant { } calc def 'Exact Satisfaction Matrix Rows' :> DocumentQueries::Query { in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))); + in candidates2 : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant::Structure"))); DocumentQueries::Project( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates)), properties = ("name"), columns = ( DocumentQueries::RelatedColumn( @@ -72,10 +81,10 @@ package Plant { maxDepth = 1, aggregate = "list", targets = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition", "OccurrenceDefinition")), exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition"))))))) + source = DocumentQueries::WhereType(source = candidates2, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition"))))))) } part def 'Exact Satisfaction Matrix Document' :> DocumentQueries::Document { attribute redefines title = "Exact Satisfaction Matrix"; @@ -94,12 +103,15 @@ package Plant { } calc def 'Wide Satisfaction Matrix Rows' :> DocumentQueries::Query { in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))); + in candidates2 : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))); DocumentQueries::Project( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates)), properties = ("name"), columns = ( DocumentQueries::RelatedColumn( @@ -108,22 +120,34 @@ package Plant { direction = "incoming", maxDepth = 1, aggregate = "list", - targets = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "OccurrenceDefinition", "RequirementDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition"))))), + targets = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates2, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates2)))), DocumentQueries::RelatedColumn( name = "Derived by", relationshipKind = "derivation", direction = "outgoing", maxDepth = 1, aggregate = "list", - targets = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "OccurrenceDefinition", "RequirementDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition"))))))) + targets = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates2, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates2)))))) } part def 'Wide Satisfaction Matrix Document' :> DocumentQueries::Document { attribute redefines title = "Wide Satisfaction Matrix"; @@ -144,11 +168,17 @@ package Plant { direction = "incoming", maxDepth = 1); DocumentQueries::Project( - source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + source = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement")), + exclude = candidates))), properties = ("qualifiedName", "@type")) } part def 'Seal Derivation Map Document' :> DocumentQueries::Document { diff --git a/tests/migrate/testdata/xmi/report_views.golden.report.txt b/tests/migrate/testdata/xmi/report_views.golden.report.txt index f4a3661aae..49fa305f52 100644 --- a/tests/migrate/testdata/xmi/report_views.golden.report.txt +++ b/tests/migrate/testdata/xmi/report_views.golden.report.txt @@ -10,11 +10,11 @@ ## approximated (10) Diagram Pump BDD _bdd -> 'Pump System'::'Pump BDD' (a diagram of unknown kind written as a view rendered asTextualNotation; the diagram names no owner; written in package 'Pump System', which holds it; no diagram representation is serialized: what the diagram is and shows is unknown, and the view exposes nothing) -Actor Stakeholders::Maintainer _maintainer -> Stakeholders::Maintainer (a UML actor is written as a part def) +Actor Stakeholders::Maintainer _maintainer -> Stakeholders::Maintainer (a UML actor is written as a part usage) «Stakeholder» Class Stakeholders::Operator _operator -> Stakeholders::Operator (a v1 «Stakeholder» is written as a part def; «Stakeholder» concern = Keep the pump running. has no v2 form) «Stakeholder» Class Stakeholders::Safety Inspector _inspector -> Stakeholders::'Safety Inspector' (a v1 «Stakeholder» is written as a part def) «Viewpoint» Class Viewpoints::Operations Viewpoint _opsVp -> Viewpoints::'Operations Viewpoint' (the stakeholder tag names _absentStakeholder, which is not in the document; v2 places the subject before the stakeholders, so an anonymous subject is declared) -«Viewpoint» Class Viewpoints::Safety Viewpoint _safetyVp -> Viewpoints::'Safety Viewpoint' (v2 places the subject before the stakeholders, so an anonymous subject is declared; SysMLv1Library::ViewpointData is unavailable, so language and presentation are retained as documentation) +«Viewpoint» Class Viewpoints::Safety Viewpoint _safetyVp -> Viewpoints::'Safety Viewpoint' (the stakeholder Stakeholders::Maintainer becomes a part, which cannot type a stakeholder; v2 places the subject before the stakeholders, so an anonymous subject is declared; SysMLv1Library::ViewpointData is unavailable, so language and presentation are retained as documentation) Package Views _views -> ViewsModel (written as ViewsModel since a root package named Views would be hidden by the standard library's Views) «Expose» Dependency Views:: _exposeReq (written as 2 relationships, one per client–supplier pair) «View» Package Views::Handbook _viewPkg -> ViewsModel::Handbook (a «View» package is written as a view usage holding its members) diff --git a/tests/migrate/testdata/xmi/report_views.golden.sysml b/tests/migrate/testdata/xmi/report_views.golden.sysml index cb7874f36a..9ff271a0cb 100644 --- a/tests/migrate/testdata/xmi/report_views.golden.sysml +++ b/tests/migrate/testdata/xmi/report_views.golden.sysml @@ -3,7 +3,7 @@ package 'Pump System' { part motor : Motor; } part def Motor; - requirement def 'Guard Fitted' { + requirement 'Guard Fitted' { doc /* The pump shall carry a guard. */ comment /* The pump shall carry a guard. */ } @@ -16,7 +16,7 @@ package Stakeholders { /* «Stakeholder» tags with no v2 form: concern = Keep the pump running. */ } part def 'Safety Inspector'; - part def Maintainer; + part Maintainer; } package Viewpoints { view def 'Safety Viewpoint' { @@ -28,7 +28,6 @@ package Viewpoints { viewpoint 'safety Viewpoint' { subject; stakeholder 'safety Inspector' : Stakeholders::'Safety Inspector'; - stakeholder maintainer : Stakeholders::Maintainer; frame 'concern'; frame concern { doc /* Is every moving part guarded? */ @@ -39,7 +38,7 @@ package Viewpoints { require constraint { doc /* Show what keeps the pump safe. */ } - metadata MigrationMetadata::SynthesizedName about 'safety Inspector', maintainer; + metadata MigrationMetadata::SynthesizedName about 'safety Inspector'; } satisfy 'safety Viewpoint'; rendering { diff --git a/tests/migrate/testdata/xmi/stereotype_filters.golden.sysml b/tests/migrate/testdata/xmi/stereotype_filters.golden.sysml index 0e56f2a660..93036857da 100644 --- a/tests/migrate/testdata/xmi/stereotype_filters.golden.sysml +++ b/tests/migrate/testdata/xmi/stereotype_filters.golden.sysml @@ -7,7 +7,7 @@ package 'Cost Profile' { } package Plant { part def Pump { - satisfy requirement : 'Flow Rate'; + satisfy 'Flow Rate'; view 'Pump Layout' { expose Pump; render Views::asTreeDiagram; @@ -28,7 +28,7 @@ package Plant { } @'Safety Profile'::Vital; } - requirement def <'D-1'> 'Flow Rate' { + requirement <'D-1'> 'Flow Rate' { doc /* The pump delivers the rated flow. */ } view 'Plant Layout' { diff --git a/tests/migrate/testdata/xmi/table_homonyms.golden.sysml b/tests/migrate/testdata/xmi/table_homonyms.golden.sysml index a730d4fa46..4d8735a0e7 100644 --- a/tests/migrate/testdata/xmi/table_homonyms.golden.sysml +++ b/tests/migrate/testdata/xmi/table_homonyms.golden.sysml @@ -88,10 +88,10 @@ package Shop { maxDepth = 1); DocumentQueries::Project( source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), exclude = DocumentQueries::Except( source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), properties = ("qualifiedName", "@type")) } part def 'Catalog Map Document' :> DocumentQueries::Document { diff --git a/tests/migrate/testdata/xmi/tables.golden.sysml b/tests/migrate/testdata/xmi/tables.golden.sysml index fe6ea8683e..181f440ddb 100644 --- a/tests/migrate/testdata/xmi/tables.golden.sysml +++ b/tests/migrate/testdata/xmi/tables.golden.sysml @@ -19,7 +19,7 @@ package Plant { attribute name : ScalarValues::String; abstract action def prime; action 'prime 2' : prime; - satisfy requirement : Plant::Requirements::FlowRequirement; + satisfy Plant::Requirements::FlowRequirement; @'Plant Profile'::Critical { level = 2; } @@ -27,7 +27,7 @@ package Plant { part def ReservePump :> Pump; part def Valve { attribute mass : ScalarValues::Real; - satisfy requirement : Plant::Requirements::SealRequirement; + satisfy Plant::Requirements::SealRequirement; } part def Station { part pumps : Pump[1..*]; @@ -45,11 +45,17 @@ package Plant { direction = "outgoing", maxDepth = 2); DocumentQueries::Project( - source = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), - exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "RequirementDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + source = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage", "OccurrenceDefinition")), + exclude = DocumentQueries::Except( + source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "ConstraintDefinition", "PortDefinition", "VerificationCaseDefinition", "ActionDefinition", "StateDefinition", "CalculationDefinition", "ViewUsage", "ViewpointUsage")))), + other = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = candidates))), properties = ("qualifiedName", "@type")) } part def 'Pump Requirement Map Document' :> DocumentQueries::Document { @@ -165,7 +171,7 @@ package Plant { individual part def s2 :> Plant::Structure::Pump; } package Requirements { - requirement def <'R-1'> FlowRequirement { + requirement <'R-1'> FlowRequirement { doc /* The pump keeps the flow above the minimum. */ comment /* The pump keeps the flow up. */ @'Plant Profile'::Critical { @@ -173,19 +179,19 @@ package Plant { level = 1; } } - requirement def <'R-2'> SealRequirement { + requirement <'R-2'> SealRequirement { doc /* Every valve seals when closed. */ comment /* Every valve seals when closed. */ @'Plant Profile'::Urgent { level = 3; } } - requirement def <'R-3'> MassRequirement { + requirement <'R-3'> MassRequirement { doc /* The pump mass stays under the limit. */ } connection def 'Derive MassRequirement' :> RequirementDerivation::Derivation { - end #RequirementDerivation::original originalRequirement : FlowRequirement; - end #RequirementDerivation::derive derivedRequirement : MassRequirement; + end #RequirementDerivation::original originalRequirement :> FlowRequirement; + end #RequirementDerivation::derive derivedRequirement :> MassRequirement; } view 'Requirement Table' { expose FlowRequirement; @@ -195,14 +201,17 @@ package Plant { render Views::asElementTable; } calc def 'Requirement Table Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Union( + source = DocumentQueries::Named(qualifiedName = ("Plant Profile", "Plant")), + other = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant Profile", "Plant")))); DocumentQueries::Project( source = DocumentQueries::OrderBy( - source = DocumentQueries::WhereType( - source = DocumentQueries::Union( - source = DocumentQueries::Named(qualifiedName = ("Plant Profile", "Plant")), - other = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant Profile", "Plant")))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = candidates)), property = "name", direction = "ascending", missing = "last", @@ -224,15 +233,18 @@ package Plant { render Views::asElementTable; } calc def 'Key Requirements Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Union( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + other = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements")))); DocumentQueries::WhereText( source = DocumentQueries::Project( source = DocumentQueries::OrderBy( - source = DocumentQueries::WhereType( - source = DocumentQueries::Union( + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = DocumentQueries::Except( source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), - other = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements")))), - type = ("RequirementDefinition")), + exclude = candidates)), property = "shortName", direction = "ascending", missing = "last", @@ -264,12 +276,15 @@ package Plant { render Views::asElementTable; } calc def 'Badly Filtered Requirements Rows' :> DocumentQueries::Query { + in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))); DocumentQueries::Project( source = DocumentQueries::OrderBy( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = candidates)), property = "shortName", direction = "ascending", missing = "last", @@ -294,12 +309,15 @@ package Plant { } calc def 'Satisfaction Matrix Rows' :> DocumentQueries::Query { in candidates : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))); + in candidates2 : KerML::Root::Element[0..*] ordered = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant::Structure"))); DocumentQueries::Project( - source = DocumentQueries::WhereType( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements"))), - type = ("RequirementDefinition")), + source = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = DocumentQueries::Except( + source = DocumentQueries::Named(qualifiedName = ("Plant::Requirements::FlowRequirement", "Plant::Requirements::SealRequirement", "Plant::Requirements::MassRequirement")), + exclude = candidates)), properties = ("name"), columns = ( DocumentQueries::RelatedColumn( @@ -309,10 +327,10 @@ package Plant { maxDepth = 1, aggregate = "list", targets = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition", "OccurrenceDefinition")), exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition"))))), + source = DocumentQueries::WhereType(source = candidates2, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition"))))), DocumentQueries::RelatedColumn( name = "Trace 2", relationshipKind = "derivation", @@ -320,10 +338,10 @@ package Plant { maxDepth = 1, aggregate = "list", targets = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition", "OccurrenceDefinition")), + source = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition", "OccurrenceDefinition")), exclude = DocumentQueries::Except( - source = DocumentQueries::WhereType(source = candidates, type = ("ItemDefinition")), - exclude = DocumentQueries::WhereType(source = candidates, type = ("PartDefinition"))))))) + source = DocumentQueries::WhereType(source = candidates2, type = ("ItemDefinition")), + exclude = DocumentQueries::WhereType(source = candidates2, type = ("PartDefinition"))))))) } part def 'Satisfaction Matrix Document' :> DocumentQueries::Document { attribute redefines title = "Satisfaction Matrix"; diff --git a/tests/migrate/testdata/xmi/vehicle.golden.report.txt b/tests/migrate/testdata/xmi/vehicle.golden.report.txt index d7b421d134..9aaa3e3123 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.report.txt +++ b/tests/migrate/testdata/xmi/vehicle.golden.report.txt @@ -1,18 +1,19 @@ # SysML v1 to v2 migration report: vehicle.xmi # exported by Example UML Tool -# migrated 93 element(s): 78 mapped, 12 approximated, 3 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 93 element(s): 77 mapped, 13 approximated, 3 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## unmapped (3) «Verify» Abstraction Requirements:: _dep_verify_block (a verify whose client is not a test case has no v2 form; applied stereotypes «Verify») «Unit» InstanceSpecification Vehicle Design::Value Types::kilogram _unit_kg (units and quantity kinds are not migrated; use the SI and ISQ libraries; applied stereotypes «Unit» (quantityKind = Vehicle Design::Value Types::mass; symbol = kg)) «QuantityKind» InstanceSpecification Vehicle Design::Value Types::mass _qk_mass (units and quantity kinds are not migrated; use the SI and ISQ libraries; applied stereotypes «QuantityKind») -## approximated (12) +## approximated (13) Package Requirements _pkg_reqs -> RequirementsModel (written as RequirementsModel since a root package named Requirements would be hidden by the standard library's Requirements) +«Refine» Abstraction Requirements:: _dep_refine -> 'Drive refines Speed Requirement' (written as a connection between usages of its ends, which a diagram draws; the standard Refinement metadata annotates only a dependency, so the connection's name says it refines) «Trace» Abstraction Requirements:: _dep_trace (a trace is written as a plain dependency) «TestCase» Activity Requirements::Mass Test _tc_mass -> RequirementsModel::'Mass Test' (the test case's behavior is not migrated; only its verified requirements are) Diagram Vehicle BDD _diag_bdd -> 'Vehicle BDD' (a diagram of unknown kind written as a view rendered asTextualNotation; the diagram names no owner; written at the top level, which holds it; no diagram representation is serialized: what the diagram is and shows is unknown, and the view exposes nothing) -Actor Vehicle Design::Driver _actor_driver -> 'Vehicle Design'::Driver (a UML actor is written as a part def) +Actor Vehicle Design::Driver _actor_driver -> 'Vehicle Design'::Driver (a UML actor is written as a part usage) «FlowPort» Port Vehicle Design::Engine::speedIn _port_speedIn -> 'Vehicle Design'::Engine::speedIn (a port typed by a DataType is written as a port holding one directed attribute) «FlowPort» Port Vehicle Design::Piston::p _port_piston_p -> 'Vehicle Design'::Piston::p (a port typed by a DataType is written as a port holding one directed attribute) «ValueType» DataType Vehicle Design::Value Types::Mass _vt_mass -> 'Vehicle Design'::'Value Types'::Mass («ValueType» quantityKind = Vehicle Design::Value Types::mass; unit = Vehicle Design::Value Types::kilogram has no v2 form) @@ -21,7 +22,7 @@ Property Vehicle Design::Vehicle::sharedCharger _prop_charger -> 'Vehicle Design «FlowPort» Port Vehicle Design::Vehicle::speedOut _port_speedOut -> 'Vehicle Design'::Vehicle::speedOut (a port typed by a DataType is written as a port holding one directed attribute) Property Vehicle Design::Vehicle::totalMass _prop_total -> 'Vehicle Design'::Vehicle::totalMass (opaque expression copied verbatim (language SysML)) -## mapped (78) +## mapped (77) Comment _cmt_model Package Empty Package _pkg_unused -> 'Empty Package' Model Model _model (the root model's members are written at the top level) @@ -29,7 +30,6 @@ Model Model _model (the root model's members are written at the top level) «Satisfy» Abstraction Requirements:: _dep_satisfy_prop -> 'Vehicle Design'::Vehicle «DeriveReqt» Abstraction Requirements:: _dep_derive -> RequirementsModel::'Derive Engine Mass Requirement' «Verify» Abstraction Requirements:: _dep_verify -> RequirementsModel::'Mass Test' -«Refine» Abstraction Requirements:: _dep_refine «Allocate» Abstraction Requirements:: _dep_allocate -> RequirementsModel::'Drive to Engine' «Allocate» Abstraction Requirements:: _dep_allocate_blocks -> RequirementsModel::'Motor to Engine' Dependency Requirements:: _dep_plain diff --git a/tests/migrate/testdata/xmi/vehicle.golden.sysml b/tests/migrate/testdata/xmi/vehicle.golden.sysml index 4e02155e68..c20e8f424d 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.sysml +++ b/tests/migrate/testdata/xmi/vehicle.golden.sysml @@ -36,7 +36,7 @@ package 'Vehicle Design' { derived attribute totalMass : 'Vehicle Design'::'Value Types'::Mass default = mass + engine.mass; part engine : Engine[1..2]; part wheels : Wheel[4..*]; - ref part driver : Driver[0..1]; + ref part driver :> Driver[0..1]; private ref part sharedCharger : Engine; constraint massLimit : MassLimit; port fuelIn : ~'Vehicle Design'::Interfaces::FuelInterface; @@ -60,8 +60,8 @@ package 'Vehicle Design' { part def Chassis { attribute mass : 'Vehicle Design'::'Value Types'::Mass; } - satisfy requirement : RequirementsModel::'Mass Requirement'; - satisfy requirement : RequirementsModel::'Engine Mass Requirement' by engine; + satisfy RequirementsModel::'Mass Requirement'; + satisfy RequirementsModel::'Engine Mass Requirement' by engine; /* «Block» tags with no v2 form: isEncapsulated = true */ } abstract part def System; @@ -85,10 +85,10 @@ package 'Vehicle Design' { part spare : Wheel :> wheels; } part def Motor :> Engine; - part def Driver; + part Driver; occurrence def Helper; connection def Drives { - end driver : Driver[0..1]; + end driver :> Driver[0..1]; end vehicle : Vehicle; } constraint def MassLimit { @@ -101,28 +101,28 @@ package 'Vehicle Design' { attribute :>> color = 'Vehicle Design'::'Value Types'::Color::blue; } action def Drive; + part vehicle : Vehicle; + connection drives : Drives connect Driver to vehicle; + action drive : Drive; } package RequirementsModel { - requirement def 'Mass Requirement' { + requirement 'Mass Requirement' { doc /* The vehicle shall have a mass of less than 1500 kg. */ - requirement def <'R1.1'> 'Chassis Mass' { + requirement <'R1.1'> 'Chassis Mass' { doc /* The chassis shall have a mass of less than 400 kg. */ } } - requirement def 'Speed Requirement' { + requirement 'Speed Requirement' { doc /* The vehicle shall reach 100 km/h "quickly". */ } - requirement def <'R1.2'> 'Engine Mass Requirement' { + requirement <'R1.2'> 'Engine Mass Requirement' { doc /* The engine shall have a mass of less than 300 kg. */ } connection def 'Derive Engine Mass Requirement' :> RequirementDerivation::Derivation { - end #RequirementDerivation::original originalRequirement : 'Mass Requirement'; - end #RequirementDerivation::derive derivedRequirement : 'Engine Mass Requirement'; + end #RequirementDerivation::original originalRequirement :> 'Mass Requirement'; + end #RequirementDerivation::derive derivedRequirement :> 'Engine Mass Requirement'; } /* not migrated: «Verify» Abstraction (_dep_verify_block) — a verify whose client is not a test case has no v2 form; applied stereotypes «Verify» */ - dependency 'Vehicle Design'::Drive to 'Speed Requirement' { - @ModelingMetadata::Refinement; - } dependency 'Speed Requirement' to 'Vehicle Design'::Engine; /* «Trace» */ allocation def 'Drive to Engine' { end :>> source : 'Vehicle Design'::Drive; @@ -135,13 +135,14 @@ package RequirementsModel { dependency 'Vehicle Design'::Wheel to 'Vehicle Design'::Engine; verification def 'Mass Test' { objective { - verify requirement : 'Mass Requirement'; + verify 'Mass Requirement'; } } metadata MigrationMetadata::SynthesizedName about 'Drive to Engine', 'Motor to Engine'; } package 'Empty Package'; metadata MigrationMetadata::LibraryNameAvoided about RequirementsModel { sourceName = "Requirements"; } +connection 'Drive refines Speed Requirement' connect 'Vehicle Design'::drive to RequirementsModel::'Speed Requirement'; view 'Vehicle BDD' { render Views::asTextualNotation; } diff --git a/tests/migrate/values_test.go b/tests/migrate/values_test.go index e78ea35ba7..17827db813 100644 --- a/tests/migrate/values_test.go +++ b/tests/migrate/values_test.go @@ -873,3 +873,15 @@ func TestConstraintSlotHoldingAVerdictIsUnmappedAsOne(t *testing.T) { wantNote(t, r, "_sl1", migrate.Unmapped, "the slot of constraint fits holds the literal SysML::Requirements::VerdictKind::pass, the run's verdict on the constraint rather than an instance of its type; an individual has no slot for a verdict") wantClean(t, "verdict-slot.sysml", r) } + +// An instance classified by an actor, a part usage once migrated, specializes +// nothing of it: an individual cannot specialize a usage. +func TestInstanceOfActorSpecializesNoUsage(t *testing.T) { + r := migrateDocument(t, ` + + `, ``) + wantLine(t, r.Notation, "/* not migrated: InstanceSpecification 'alice' — the instance's classifier Driver is written as a part usage, which an individual cannot specialize */") + wantNoLine(t, r.Notation, "individual part alice") + wantNote(t, r, "_alice", migrate.Unmapped, "the instance's classifier Driver is written as a part usage, which an individual cannot specialize") + wantClean(t, "actor-instance.sysml", r) +}