From a2398c753478eed3f9ce7ec7df8950ad515b7f09 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:04:51 +0000 Subject: [PATCH 01/12] feat(migrate): use case diagrams draw actor associations, includes, extends and refines as connections between usages Co-Authored-By: jason.han --- .../use-case-connections.changed.md | 1 + docs/reference/sysml-v1-migration.md | 17 +- internal/ir/view/interconnection.go | 14 +- internal/translate/migrate/diagrams.go | 44 ++- .../migrate/diagrams_internal_test.go | 13 +- internal/translate/migrate/edges.go | 9 + internal/translate/migrate/migrate.go | 29 +- internal/translate/migrate/usecase.go | 283 +++++++++++++++++- .../migrate/usecase_internal_test.go | 4 +- tests/migrate/layout_test.go | 4 +- tests/migrate/migrate_test.go | 2 +- tests/migrate/profile_test.go | 9 +- tests/migrate/relations_test.go | 22 +- .../xmi/diagram_edges.golden.report.txt | 2 +- .../testdata/xmi/diagram_edges.golden.sysml | 13 +- .../diagram_edges.layout.golden.report.txt | 8 +- .../xmi/diagram_edges.layout.golden.sysml | 18 +- .../testdata/xmi/diagrams.golden.report.txt | 10 +- .../testdata/xmi/diagrams.golden.sysml | 13 +- .../xmi/parking_usecases.golden.report.txt | 4 +- .../xmi/parking_usecases.golden.sysml | 19 +- .../xmi/profile_inheritance.golden.report.txt | 8 +- .../xmi/profile_inheritance.golden.sysml | 10 +- .../testdata/xmi/vehicle.golden.report.txt | 8 +- .../migrate/testdata/xmi/vehicle.golden.sysml | 9 +- 25 files changed, 489 insertions(+), 84 deletions(-) create mode 100644 changes/unreleased/use-case-connections.changed.md diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md new file mode 100644 index 0000000000..0db9bd9a6a --- /dev/null +++ b/changes/unreleased/use-case-connections.changed.md @@ -0,0 +1 @@ +- The SysML v1 migration writes the lines a use case diagram draws as connections a tool can show: each actor association, «Include», Extend and «Refine» becomes a `connection` between `part`, `use case` or `requirement` usages of its ends, written once per definition in its own package only when a connection joins it, hosted by the nearest package holding both ends; a named association's one usage is typed by its `connection def` and named like a usage. A «Refine» keeps its other metadata in the connection's body and the refine as a comment, reported as approximated. 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. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 1ac8bd2cc1..37c67ada76 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -131,7 +131,7 @@ 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 def`; one `part` usage of it in its package when an association, drawn as a `connection`, names it | approximated | | «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 | @@ -154,7 +154,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 `part`/`use case` usages written in the ends' packages, 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 | @@ -181,7 +181,7 @@ ties each of its mapping classes to the code that carries it out (or records why | «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 connect to ; /* «Refine» */` between usages of its ends written in their packages (a `part`, `use case`, `requirement`, `action`, `occurrence` or `item` usage of the definition), other metadata in its body — a line a diagram draws; the standard `Refinement` metadata annotates only a dependency, so the refine is kept as a comment. A «Refine» whose end is no such definition 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,9 +204,9 @@ 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 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, and a `connection` between a `part` usage of the actor and a `use case` usage of the case in their packages, which a use case diagram draws; a `classifierBehavior` is performed as in a block | mapped | +| Include | `include use case : ;` and `connection … connect to ; /* «include» */` between `use case` usages of the two cases, which a diagram draws | mapped | +| Extend, ExtensionPoint | `connection connect to ;` between `use case` usages of the two cases, the extension points and condition as a comment; 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 | | «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 | @@ -381,7 +381,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 `part`, `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,7 +415,7 @@ 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 | +| Dependency, Extend, «Refine» | `dependency 'A to B' from A to B;` (`allocation` for an «Allocate», `connection 'a to b' connect a to b;` for an Extend or a «Refine» between usages); 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 …` | diff --git a/internal/ir/view/interconnection.go b/internal/ir/view/interconnection.go index 3413bd47f3..127f316bf1 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): @@ -235,12 +235,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) { out = append(out, member) } } @@ -532,6 +532,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/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 720e7d5a2b..7f7798c2df 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 conn := m.connRefs(shown.Element, host); len(conn) > 0 { + // The connection stands for the edge; its members would shadow the usages. + for _, also := range conn { + add(also) + } + continue + } for _, also := range m.edgeRefs(shown.Element, host) { add(also) } @@ -734,6 +744,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 +912,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 +933,7 @@ type layoutPlan struct { refOf map[string]string written int unexposed int + onEdges int dangling int reasons map[string]int } @@ -935,9 +961,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 +1060,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 +1072,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..1f8024d4b5 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -211,6 +211,8 @@ 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{}, monteCarlo: map[*sysmlv1.Element]*monteCarloCase{}, strict: opts.Strict, layout: opts.Layout, @@ -528,6 +530,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 + defUsageList []*defUsage + conns 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 +652,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 +717,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 +768,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 +782,7 @@ func (m *migration) prepare() { } m.indexSnapshots(configs) m.placeActors(links) + m.planConnections(joined) for _, e := range reachers { m.exposeReached(e) } @@ -1145,7 +1159,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. @@ -3592,6 +3606,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.conns[d] + if conn == nil { + conn = m.usageConnection(d, name, client, supplier, " /* «Refine» */") + } + 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" diff --git a/internal/translate/migrate/usecase.go b/internal/translate/migrate/usecase.go index 5db4f51dbb..73644dc213 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -14,6 +14,264 @@ type actorLink struct { // actor usage takes; name is the usage's name once placed. end *sysmlv1.Element name string + // conn is the connection written beside the use case 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 a usage of a definition written in a package so a connection can +// join it: a connection joins features, which a v1 actor, use case or requirement is not. +type defUsage struct { + def, host *sysmlv1.Element + keyword, name string +} + +// 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 +} + +// 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 refine is kept as a comment" + +// 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 catUseCaseDef: + return "use case" + case catRequirementDef: + 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 of def in host's body, declared there 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{} + } + 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.defUsageList = append(m.defUsageList, u) + m.extras[host] = append(m.extras[host], func() { + m.w.line(keyword + " " + writeName(u.name) + " : " + m.ref(def, host) + ";") + }) + return u +} + +// 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 definitions, each in its own package, and records it as +// the edge e is drawn as. +func (m *migration) connectUsages(e, host, from, to *sysmlv1.Element, fromKw, toKw, name, trailer string) *defConn { + return m.connectTyped(e, nil, host, from, to, fromKw, toKw, name, trailer) +} + +// 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, trailer string) *defConn { + fu, tu := m.defUsageOf(m.usageHost(from), from, fromKw), m.defUsageOf(m.usageHost(to), to, toKw) + if name == "" { + name = m.freshName(host, spoken(writeName(fu.name))+" to "+spoken(writeName(tu.name))) + } + c := &defConn{of: e, host: host, name: name, from: fu, to: tu, def: def} + m.conns[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, ";"+trailer, strings.TrimSpace(trailer), func() { + if def != nil { + return // the connection def carries the association's metadata + } + 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 { + return m.hostedRef(u.host, u.name, scope) +} + +// connectActor writes the connection a link's association is drawn as, between +// a part usage of the actor and a use case usage, in the package owning the association. +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, m.freshName(host, lowerFirst(name)) + } + link.conn = m.connectTyped(link.assoc, def, host, from, to, fromKw, toKw, name, "") +} + +// usageConnection writes a relationship d between two definitions as a +// connection between their usages in the package d is written in, trailer +// closing the line; nil when either end is no definition a usage can be written of. +func (m *migration) usageConnection(d *sysmlv1.Element, name string, client, supplier *sysmlv1.Element, trailer string) *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, trailer) +} + +// 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 { + c := m.conns[e] + if c == nil { + return nil + } + return []string{m.hostedRef(c.host, c.name, scope), m.usageRef(c.from, scope), m.usageRef(c.to, scope)} +} + +// planConnections writes, before any view is, the connection each include, +// extend and «Refine» between definitions is drawn as, so a view exposes the +// usages the connections join wherever in the model they are hosted. +func (m *migration) planConnections(rels []*sysmlv1.Element) { + for _, r := range rels { + var from, to *sysmlv1.Element + trailer := "" + switch r.Type { + case "Include": + from, to, trailer = r.Parent, m.model.Ref(r, "addition"), " /* «include» */" + if to != nil && m.written(to) { + if cat, _ := m.classify(to); cat != catUseCaseDef { + continue + } + } + case "Extend": + from, to, trailer = r.Parent, m.model.Ref(r, "extendedCase"), " /* "+m.extendComment(r)+" */" + default: + clients, suppliers := m.model.Refs(r, "client"), m.model.Refs(r, "supplier") + if len(clients) != 1 || len(suppliers) != 1 { + continue + } + from, to, trailer = clients[0], suppliers[0], " /* «Refine» */" + } + if from == nil || to == nil || !m.written(from) || !m.written(to) { + continue + } + m.usageConnection(r, m.nameOf(r), from, to, trailer) + } +} + +// 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 +287,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) { @@ -44,6 +308,10 @@ func (m *migration) actorLink(a *sysmlv1.Element) *actorLink { // of the use cases is named, and registers it in the use case's body. func (m *migration) placeActors(links []*actorLink) { for _, link := range links { + if link.block { + m.connectActor(link) + continue + } base := m.nameOf(link.end) if base == "" { base = lowerFirst(link.actor.Name) @@ -51,6 +319,7 @@ func (m *migration) placeActors(links []*actorLink) { 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) }) + m.connectActor(link) } } @@ -169,6 +438,9 @@ func (m *migration) include(inc *sysmlv1.Element) { 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) + ";") + if conn := m.conns[inc]; conn != nil { + m.wroteEdgeAlso(inc, conn.host, "connection", nil, conn.name) + } m.add(inc, Mapped, m.v2Name(inc), "") m.stereotypeComments(inc) } @@ -187,11 +459,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..08e982f419 100644 --- a/internal/translate/migrate/usecase_internal_test.go +++ b/internal/translate/migrate/usecase_internal_test.go @@ -90,13 +90,13 @@ func TestUseCaseForms(t *testing.T) { `, "", - []string{"dependency Buy to Browse; /* extends at extension point(s) 'found' when true */", + []string{"connection 'buy to browse' connect buy to browse; /* extends at extension point(s) 'found' when true */", "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 to browse' connect buy to browse; /* extends at extension point(s) _gone (not in the document) */"}, "_ext", Approximated}, {"a block property typed by a use case is a reference use case usage", ` 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..e9e3585263 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -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, diff --git a/tests/migrate/profile_test.go b/tests/migrate/profile_test.go index cd1aad8644..20269c5d84 100644 --- a/tests/migrate/profile_test.go +++ b/tests/migrate/profile_test.go @@ -77,19 +77,24 @@ func TestInheritedStereotypeSemanticsFollowEveryGeneral(t *testing.T) { 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, "connection 'spin to speed Requirement' connect spin to 'speed Requirement' {") + wantLine(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..102b2a3050 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1384,7 +1384,7 @@ 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, "connection 'ref' connect thing to req; /* «Refine» */") wantLine(t, r.Notation, "allocation def alloc {") wantLine(t, r.Notation, "end :>> source : Thing;") wantLine(t, r.Notation, "end :>> target : Piece;") @@ -1396,7 +1396,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"}, @@ -1676,3 +1676,21 @@ func TestItemFlowWithEndsStandingForBothIsNotMigrated(t *testing.T) { } wantClean(t, "t.sysml", r) } + +// 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 : Driver;", "part bank : Bank;", "use case 'park Car' : 'Park Car';", "use case 'pay Fee' : 'Pay Fee';"} { + wantLine(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 ") +} diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt index 4e58721006..4852c80c95 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt @@ -79,6 +79,6 @@ 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) +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..6367eb096d 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml @@ -133,11 +133,16 @@ package Usage { } use case def 'Start Engine'; use case def 'Stop Engine'; + use case 'drive Vehicle' : 'Drive Vehicle'; + use case 'start Engine' : 'Start Engine'; + connection 'drive Vehicle to start Engine' connect 'drive Vehicle' to 'start Engine'; /* «include» */ + use case 'stop Engine' : 'Stop Engine'; + connection 'drive Vehicle to stop Engine' connect 'drive Vehicle' to 'stop Engine'; /* «include» */ view 'Driving Cases' { - expose 'Drive Vehicle'; - expose 'Start Engine'; - expose Usage::'Drive Vehicle'::'start Engine'; - render Views::asTextualNotation; + expose 'drive Vehicle'; + expose 'start Engine'; + expose 'drive Vehicle to 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..ea47dc534f 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 @@ -100,6 +100,6 @@ 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)) +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..50fc4b5bac 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml @@ -165,14 +165,20 @@ package Usage { } use case def 'Start Engine'; use case def 'Stop Engine'; + use case 'drive Vehicle' : 'Drive Vehicle'; + use case 'start Engine' : 'Start Engine'; + connection 'drive Vehicle to start Engine' connect 'drive Vehicle' to 'start Engine'; /* «include» */ + use case 'stop Engine' : 'Stop Engine'; + connection 'drive Vehicle to stop Engine' connect 'drive Vehicle' to 'stop Engine'; /* «include» */ view 'Driving Cases' { - expose 'Drive Vehicle'; - expose 'Start Engine'; - expose Usage::'Drive Vehicle'::'start Engine'; + expose 'drive Vehicle'; + expose 'start Engine'; + expose 'drive Vehicle to 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::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; } + metadata DiagramLayout::Route about 'drive Vehicle to 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..4928468b9c 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagrams.golden.report.txt @@ -1,14 +1,14 @@ # SysML v1 to v2 migration report: diagrams.xmi # exported by Example UML Tool -# migrated 62 element(s): 46 mapped, 16 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 62 element(s): 47 mapped, 15 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (16) +## approximated (15) OpaqueAction Behavior::Drive::start engine _act_start -> 'start engine' (the JavaScript body is translated to v2; running is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it) Diagram Behavior::Drive::start engine _diag_act_node -> Behavior::Drive::'start engine 2' (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Behavior::Drive, whose graph the rendering draws with the 1 shown nodes and edges of it; written as start engine 2 since a member of its owner is also named start engine) 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) @@ -18,9 +18,8 @@ Diagram Structure::Vehicle::engine::Engine Matrix _diag_matrix -> Structure::Veh Property Usage:::: _end_uc -> Usage::'Drive Vehicle'::driver (the end at the use case is the use case def the actor is declared in) 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) -## mapped (46) +## mapped (47) Package Behavior _pkg_behavior -> Behavior Activity Behavior::Drive _act_drive -> Behavior::Drive ActivityFinalNode Behavior::Drive:: _final -> final @@ -67,6 +66,7 @@ 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) +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..d56e8d7cd9 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 { @@ -118,11 +118,14 @@ package Usage { subject; actor driver : Driver; } + part driver : Driver; + use case 'drive Vehicle' : '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; + expose 'drive Vehicle'; + expose 'driver to drive Vehicle'; + render Views::asInterconnectionDiagram; } } package Reports { diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt index dab7b55b9a..6a898a2290 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt @@ -4,7 +4,7 @@ ## 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) @@ -18,7 +18,7 @@ OpaqueAction Use Cases::Park Car::Park Flow::Enter _pfEnter -> Enter (the body i 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) 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 to 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) Package Context _ctx -> Context diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml index fdb21ebbbd..8bc1a000f0 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml @@ -6,13 +6,16 @@ package Context { } part def Attendant; part def Bank; + part driver : Driver; + part attendant : Attendant; + part bank : Bank; } package 'Use Cases' { use case def '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 */ + /* 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 { @@ -39,9 +42,7 @@ package 'Use Cases' { subject; 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 'Wait For Space'; use case def '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) */ @@ -61,6 +62,16 @@ package 'Use Cases' { end 'wait For Space' : 'Wait For Space'; metadata MigrationMetadata::SynthesizedName about 'park Car', 'wait For Space'; } + use case 'park Car' : 'Park Car'; + use case 'pay Fee' : 'Pay Fee'; + connection 'park Car to pay Fee' connect 'park Car' to 'pay Fee'; /* «include» */ + use case 'authorise Payment' : 'Authorise Payment'; + connection authorise connect 'pay Fee' to 'authorise Payment'; /* «include» */ + use case 'wait For Space' : 'Wait For Space'; + connection 'wait For Space to park Car' connect 'wait For Space' to 'park Car'; /* 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..66a26cf722 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 to 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 refine is kept as a comment) «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..485b994083 100644 --- a/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml +++ b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml @@ -61,10 +61,6 @@ 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'; @@ -79,5 +75,11 @@ package System { end :>> target : Shaft; @Tailoring::'Assigned To'; } + action spin : Spin; + requirement 'speed Requirement' : 'Speed Requirement'; + connection 'spin to speed Requirement' connect spin to 'speed Requirement' { + /* «Refine» */ + @Tailoring::'Detailed By'; + } metadata MigrationMetadata::SynthesizedName about 'Spin to Shaft'; } diff --git a/tests/migrate/testdata/xmi/vehicle.golden.report.txt b/tests/migrate/testdata/xmi/vehicle.golden.report.txt index d7b421d134..efd9c0246b 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.report.txt +++ b/tests/migrate/testdata/xmi/vehicle.golden.report.txt @@ -1,14 +1,15 @@ # 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 to 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 refine is kept as a comment) «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) @@ -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..1cca68861a 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.sysml +++ b/tests/migrate/testdata/xmi/vehicle.golden.sysml @@ -101,6 +101,10 @@ package 'Vehicle Design' { attribute :>> color = 'Vehicle Design'::'Value Types'::Color::blue; } action def Drive; + part driver : Driver; + part vehicle : Vehicle; + connection drives : Drives connect driver to vehicle; + action drive : Drive; } package RequirementsModel { requirement def 'Mass Requirement' { @@ -120,9 +124,6 @@ package RequirementsModel { 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; @@ -138,10 +139,12 @@ package RequirementsModel { verify requirement : 'Mass Requirement'; } } + requirement 'speed Requirement' : 'Speed Requirement'; metadata MigrationMetadata::SynthesizedName about 'Drive to Engine', 'Motor to Engine'; } package 'Empty Package'; metadata MigrationMetadata::LibraryNameAvoided about RequirementsModel { sourceName = "Requirements"; } +connection 'drive to speed Requirement' connect 'Vehicle Design'::drive to RequirementsModel::'speed Requirement'; /* «Refine» */ view 'Vehicle BDD' { render Views::asTextualNotation; } From ddd3091ed7541568baa8c34f39643cf4f324d107 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:10:21 +0000 Subject: [PATCH 02/12] test(migrate): pin the vehicle report counts a refine written as a connection moves (77 mapped, 13 approximated) Co-Authored-By: jason.han --- client/julia/OpenSysML/test/extended_client.jl | 4 ++-- client/matlab/tests/test_surface_live.m | 4 ++-- client/node/test/migrate.test.ts | 6 +++--- client/python/opensysml/__init__.py | 2 +- client/rust/opensysml/tests/surface.rs | 4 ++-- conformance/scenarios/16-migrate.json | 6 +++--- docs/reference/wire-contract.md | 6 +++--- 7 files changed, 16 insertions(+), 16 deletions(-) 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/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 } From afa0b313bf307099631cb2e80876e76e610ce02c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:09:36 +0000 Subject: [PATCH 03/12] =?UTF-8?q?fix(migrate):=20a=20=C2=ABRefine=C2=BB=20?= =?UTF-8?q?with=20several=20ends=20is=20a=20connection=20per=20pair;=20con?= =?UTF-8?q?nection=20names=20yield=20to=20package=20members;=20use=20case?= =?UTF-8?q?=20views=20expose=20an=20association=20end's=20usages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dependency with several clients or suppliers now writes one connection per client–supplier pair, each named distinctly and each exposed by the views that show the dependency. A connection named after its relationship takes the name only when no member of the package it is written in has it. A use case view showing only an association end exposes the connection and both usages it joins, and only use case views expose those usages. Co-Authored-By: jason.han --- internal/translate/migrate/diagrams.go | 4 +- internal/translate/migrate/migrate.go | 22 +++-- internal/translate/migrate/names.go | 65 +++++++++++++- internal/translate/migrate/usecase.go | 38 +++++++- tests/migrate/relations_test.go | 119 +++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 15 deletions(-) diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 7f7798c2df..37308d40d3 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -662,9 +662,9 @@ func (m *migration) exposures(d *sysmlv1.Diagram, host *sysmlv1.Element, form vi x.unwritten++ default: add(ref) - if conn := m.connRefs(shown.Element, host); len(conn) > 0 { + 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 conn { + for _, also := range m.connRefs(c.of, host) { add(also) } continue diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 1f8024d4b5..f946be8bbd 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{}, @@ -213,6 +214,7 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result { 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, @@ -368,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 @@ -535,6 +539,7 @@ type migration struct { defUsages map[*sysmlv1.Element]map[*sysmlv1.Element]*defUsage defUsageList []*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. @@ -3491,9 +3496,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 { @@ -3518,8 +3525,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) @@ -3607,7 +3619,7 @@ func (m *migration) dependencyPair(d *sysmlv1.Element, pl *placement, name strin target = m.qualified(append(m.segments(m.scope), name)) } if has(d, "Refine") { - conn := m.conns[d] + conn := m.pairConn(d, client, supplier) if conn == nil { conn = m.usageConnection(d, name, client, supplier, " /* «Refine» */") } diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 0127cd7d9f..3cacd3b9d4 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -115,14 +115,20 @@ 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 } } @@ -132,13 +138,18 @@ func (m *migration) nameTaken(owner *sysmlv1.Element, name string) bool { // 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 { + return m.topLevelNamedBut(nil, name) +} + +// 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 +158,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 diff --git a/internal/translate/migrate/usecase.go b/internal/translate/migrate/usecase.go index 73644dc213..51b1464fb9 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -120,10 +120,20 @@ func (m *migration) connectUsages(e, host, from, to *sysmlv1.Element, fromKw, to func (m *migration) connectTyped(e, def, host, from, to *sysmlv1.Element, fromKw, toKw, name, trailer string) *defConn { fu, tu := m.defUsageOf(m.usageHost(from), from, fromKw), m.defUsageOf(m.usageHost(to), to, toKw) if name == "" { - name = m.freshName(host, spoken(writeName(fu.name))+" to "+spoken(writeName(tu.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} - m.conns[e] = c + 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 { @@ -164,7 +174,7 @@ func (m *migration) connectActor(link *actorLink) { 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, m.freshName(host, lowerFirst(name)) + def, name = link.assoc, lowerFirst(name) } link.conn = m.connectTyped(link.assoc, def, host, from, to, fromKw, toKw, name, "") } @@ -187,11 +197,31 @@ func (m *migration) usageConnection(d *sysmlv1.Element, name string, client, sup // 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 []string{m.hostedRef(c.host, c.name, scope), m.usageRef(c.from, scope), m.usageRef(c.to, scope)} + 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, diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index 102b2a3050..e4e7274384 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1,6 +1,7 @@ package migrate_test import ( + "fmt" "strings" "testing" @@ -1694,3 +1695,121 @@ func TestUsagesOnlyForConnectedDefinitions(t *testing.T) { 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 to req' connect alpha to req; /* «Refine» */") + wantLine(t, r.Notation, "connection 'beta to req' connect beta to req; /* «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; /* «Refine» */") + wantLine(t, r.Notation, "connection 'Link 2' connect beta to req; /* «Refine» */") + 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 def Link;") + wantLine(t, r.Notation, "connection 'Link 2' connect alpha to req; /* «Refine» */") + 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) + } + } +} From 64d4cfca6ac34592eccf3651b5fbb44aae4c95eb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:28:12 +0000 Subject: [PATCH 04/12] chore(migrate): drop the unused topLevelNamed helper Co-Authored-By: jason.han --- internal/translate/migrate/names.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 3cacd3b9d4..1b97935194 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -135,12 +135,6 @@ func (m *migration) nameTakenBut(e, owner *sysmlv1.Element, name string) bool { 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 { - return m.topLevelNamedBut(nil, name) -} - // topLevelNamedBut is topLevelNamed disregarding e (see nameTakenBut). func (m *migration) topLevelNamedBut(e *sysmlv1.Element, name string) bool { for _, r := range m.model.Roots { From f6c13816043829f8b417c4f8260cabf985589541 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:46:04 +0000 Subject: [PATCH 05/12] feat(migrate): actors and use cases are usages; use case connections named by their kind A UML Actor is written as a part usage and a UseCase as a use case usage (subject, includes and behavior inside it), so each is one element a tool draws once and a connection can join. Actor associations, includes, extends and refines are connections named by their kind with no trailing comment, an extend's points and condition as the connection's doc. The interconnection renderer draws an include as the connection's line, not a nested node. Co-Authored-By: jason.han --- .../use-case-connections.changed.md | 2 +- docs/guide/11-migrating-from-sysml-v1.md | 4 +- docs/reference/sysml-v1-migration.md | 20 +- internal/ir/view/interconnection.go | 4 +- .../ir/view/interconnection_includes_test.go | 23 +++ .../testdata/interconnection-includes.sysml | 22 ++ internal/translate/migrate/classify.go | 25 ++- internal/translate/migrate/diagrams.go | 9 +- internal/translate/migrate/migrate.go | 33 +-- internal/translate/migrate/names.go | 2 +- .../synthesized_names_internal_test.go | 2 +- internal/translate/migrate/typefilter.go | 4 +- internal/translate/migrate/uniqueness.go | 3 +- internal/translate/migrate/usecase.go | 192 +++++++++--------- .../migrate/usecase_internal_test.go | 32 +-- internal/translate/migrate/views.go | 2 +- tests/migrate/migrate_test.go | 2 +- tests/migrate/profile_test.go | 4 +- tests/migrate/relations_test.go | 33 +-- .../xmi/diagram_edges.golden.report.txt | 4 +- .../testdata/xmi/diagram_edges.golden.sysml | 24 +-- .../diagram_edges.layout.golden.report.txt | 4 +- .../xmi/diagram_edges.layout.golden.sysml | 30 ++- .../testdata/xmi/diagrams.golden.report.txt | 16 +- .../testdata/xmi/diagrams.golden.sysml | 17 +- .../xmi/parking_usecases.golden.report.txt | 36 ++-- .../xmi/parking_usecases.golden.sysml | 60 +++--- .../xmi/profile_inheritance.golden.report.txt | 2 +- .../xmi/profile_inheritance.golden.sysml | 3 +- .../xmi/report_views.golden.report.txt | 4 +- .../testdata/xmi/report_views.golden.sysml | 5 +- .../testdata/xmi/vehicle.golden.report.txt | 4 +- .../migrate/testdata/xmi/vehicle.golden.sysml | 11 +- 33 files changed, 348 insertions(+), 290 deletions(-) create mode 100644 internal/ir/view/interconnection_includes_test.go create mode 100644 internal/ir/view/testdata/interconnection-includes.sysml diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md index 0db9bd9a6a..fc7c4f9766 100644 --- a/changes/unreleased/use-case-connections.changed.md +++ b/changes/unreleased/use-case-connections.changed.md @@ -1 +1 @@ -- The SysML v1 migration writes the lines a use case diagram draws as connections a tool can show: each actor association, «Include», Extend and «Refine» becomes a `connection` between `part`, `use case` or `requirement` usages of its ends, written once per definition in its own package only when a connection joins it, hosted by the nearest package holding both ends; a named association's one usage is typed by its `connection def` and named like a usage. A «Refine» keeps its other metadata in the connection's body and the refine as a comment, reported as approximated. 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. +- 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; an extend's extension points and condition are the connection's `doc`. 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. 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/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 37c67ada76..89123ace52 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -131,7 +131,7 @@ 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`; one `part` usage of it in its package when an association, drawn as a `connection`, names it | 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 | @@ -154,7 +154,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`; an association to an Actor also has its one usage, `connection : connect to ;` between `part`/`use case` usages written in the ends' packages, hosted by the nearest package holding both | 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 | @@ -181,7 +181,7 @@ ties each of its mapping classes to the code that carries it out (or records why | «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» | `connection connect to ; /* «Refine» */` between usages of its ends written in their packages (a `part`, `use case`, `requirement`, `action`, `occurrence` or `item` usage of the definition), other metadata in its body — a line a diagram draws; the standard `Refinement` metadata annotates only a dependency, so the refine is kept as a comment. A «Refine» whose end is no such definition stays a `dependency` carrying `@ModelingMetadata::Refinement` | approximated | +| «Refine» | `connection ' refines ' connect to ;` between usages of its ends (an actor's `part`, a `use case`, or a `part`, `requirement`, `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 +204,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, and a `connection` between a `part` usage of the actor and a `use case` usage of the case in their packages, which a use case diagram draws; a `classifierBehavior` is performed as in a block | mapped | -| Include | `include use case : ;` and `connection … connect to ; /* «include» */` between `use case` usages of the two cases, which a diagram draws | mapped | -| Extend, ExtensionPoint | `connection connect to ;` between `use case` usages of the two cases, 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 +381,7 @@ 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` | -| a use case diagram | `Views::asInterconnectionDiagram` over the `part`, `use case` and `requirement` usages and the connections written for the actor associations, includes, extends and «Refine»s it shows | +| a use case diagram | `Views::asInterconnectionDiagram` over the actors' `part` usages, the `use case` usages, the `requirement` usages written for refined requirements 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 @@ -415,10 +415,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, «Refine» | `dependency 'A to B' from A to B;` (`allocation` for an «Allocate», `connection 'a to b' connect a to b;` for an Extend or a «Refine» between usages); several clients or suppliers are several dependencies, one per pair, and the view exposes each | +| 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); 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 …` | +| Include, Message | `connection 'A includes B' connect A to B;` for an Include; 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 diff --git a/internal/ir/view/interconnection.go b/internal/ir/view/interconnection.go index 127f316bf1..ba643b93fb 100644 --- a/internal/ir/view/interconnection.go +++ b/internal/ir/view/interconnection.go @@ -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 @@ -240,7 +242,7 @@ func (r *Renderer) libraryDeclared(sym *symbols.Symbol) bool { func (w *featureWalk) nestedFeatures(sym *symbols.Symbol) []*symbols.Symbol { var out []*symbols.Symbol for _, member := range w.r.containedMembers(sym) { - if w.drawsFeature(member) { + if w.drawsFeature(member) && !isReferencedIncludedCase(member) { out = append(out, member) } } 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/translate/migrate/classify.go b/internal/translate/migrate/classify.go index 852de58b1f..8ad1374ee7 100644 --- a/internal/translate/migrate/classify.go +++ b/internal/translate/migrate/classify.go @@ -31,8 +31,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. @@ -85,8 +88,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: @@ -132,8 +137,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 +505,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 +535,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": @@ -572,7 +579,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" } diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 37308d40d3..610f9f6cbb 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -713,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 @@ -727,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 diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index f946be8bbd..03d5a3762c 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -536,10 +536,9 @@ type migration struct { 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 - defUsageList []*defUsage - conns map[*sysmlv1.Element]*defConn - moreConns map[*sysmlv1.Element][]*defConn + 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. @@ -1413,7 +1412,7 @@ func (m *migration) classifierBody(e *sysmlv1.Element, cat category, header stri case catRequirementDef: m.w.block(header, func() { m.requirementBody(e) }) return - case catUseCaseDef: + case catUseCase: m.w.block(header, func() { m.useCaseBody(e) }) return case catView: @@ -2107,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) { @@ -2117,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 != "" { @@ -2230,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) @@ -2302,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: @@ -2567,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 { return " :> " } } @@ -3621,7 +3626,11 @@ func (m *migration) dependencyPair(d *sysmlv1.Element, pl *placement, name strin if has(d, "Refine") { conn := m.pairConn(d, client, supplier) if conn == nil { - conn = m.usageConnection(d, name, client, supplier, " /* «Refine» */") + connName := name + if m.nameOf(d) == "" { + connName = spoken(writeName(m.nameFor(client))) + " refines " + spoken(writeName(m.nameFor(supplier))) + } + conn = m.usageConnection(d, connName, client, supplier) } if conn != nil { m.wroteEdgeAlso(d, conn.host, "connection", nil, conn.name) diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 1b97935194..a6ae7da7bb 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -354,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 } // scopeChain lists scope and its ancestors, innermost first, stopping at the 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/typefilter.go b/internal/translate/migrate/typefilter.go index 3b498b4278..1edc8af54d 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -146,7 +146,7 @@ var metaclassTypes = map[string]v2Types{ "Classifier": {types: classifierTypes, note: noteClassifierExtra}, "Class": occurrenceAwareTypes(classTypes, typeOccurrenceDef, typeItemDef, ""), "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": {types: []string{typePartUsage}, note: "an actor is a part usage once migrated"}, "Behavior": {types: behaviorTypes}, "Activity": {types: []string{typeActionDef, typeCalcDef}}, "OpaqueBehavior": {types: []string{typeActionDef, typeCalcDef}}, @@ -154,7 +154,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": {types: []string{typeUseCaseUsage}}, "DataType": {types: []string{typeAttributeDef, typeEnumDef}}, "PrimitiveType": {types: []string{typeAttributeDef}}, "Enumeration": {types: []string{typeEnumDef}}, diff --git a/internal/translate/migrate/uniqueness.go b/internal/translate/migrate/uniqueness.go index d9ad2b411f..59ad2410af 100644 --- a/internal/translate/migrate/uniqueness.go +++ b/internal/translate/migrate/uniqueness.go @@ -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 51b1464fb9..d488b09903 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -6,26 +6,28 @@ 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 - // conn is the connection written beside the use case for the association, nil when no package holds it. + // 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 a usage of a definition written in a package so a connection can -// join it: a connection joins features, which a v1 actor, use case or requirement is not. +// 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, @@ -37,6 +39,8 @@ type defConn struct { 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. @@ -45,7 +49,7 @@ func (c *defConn) target(m *migration) string { } // 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 refine is kept as a comment" +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. @@ -57,8 +61,10 @@ func (m *migration) usageKeyword(def *sysmlv1.Element) string { switch cat { case catPartDef: return "part" - case catUseCaseDef: + case catUseCase: return "use case" + case catActor: + return "part" case catRequirementDef: return "requirement" case catActionDef: @@ -82,7 +88,9 @@ func (m *migration) usageHost(e *sysmlv1.Element) *sysmlv1.Element { return nil } -// defUsageOf is the usage of def in host's body, declared there on first demand. +// 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 @@ -90,34 +98,43 @@ func (m *migration) defUsageOf(host, def *sysmlv1.Element, keyword string) *defU 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.defUsageList = append(m.defUsageList, 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 +} + // 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 definitions, each in its own package, and records it as -// the edge e is drawn as. -func (m *migration) connectUsages(e, host, from, to *sysmlv1.Element, fromKw, toKw, name, trailer string) *defConn { - return m.connectTyped(e, nil, host, from, to, fromKw, toKw, name, trailer) +// 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, trailer string) *defConn { +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)) @@ -140,10 +157,13 @@ func (m *migration) connectTyped(e, def, host, from, to *sysmlv1.Element, fromKw typed = " : " + m.ref(def, host) } header := "connection " + writeName(name) + typed + " connect " + m.usageRef(fu, host) + " to " + m.usageRef(tu, host) - m.w.trailed(header, ";"+trailer, strings.TrimSpace(trailer), func() { + 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) @@ -155,11 +175,15 @@ func (m *migration) connectTyped(e, def, host, from, to *sysmlv1.Element, fromKw // 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 drawn as, between -// a part usage of the actor and a use case usage, in the package owning the association. +// 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 { @@ -176,13 +200,30 @@ func (m *migration) connectActor(link *actorLink) { // 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, "") + 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 definitions as a -// connection between their usages in the package d is written in, trailer -// closing the line; nil when either end is no definition a usage can be written of. -func (m *migration) usageConnection(d *sysmlv1.Element, name string, client, supplier *sysmlv1.Element, trailer string) *defConn { +// 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 @@ -191,7 +232,7 @@ func (m *migration) usageConnection(d *sysmlv1.Element, name string, client, sup if !ok { return nil } - return m.connectUsages(d, host, client, supplier, ck, sk, name, trailer) + return m.connectUsages(d, host, client, supplier, ck, sk, name) } // connRefs are the references a view exposing e also exposes so the connection @@ -225,33 +266,41 @@ func (m *migration) pairConn(d, client, supplier *sysmlv1.Element) *defConn { } // planConnections writes, before any view is, the connection each include, -// extend and «Refine» between definitions is drawn as, so a view exposes the -// usages the connections join wherever in the model they are hosted. +// 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 - trailer := "" + verb := "" switch r.Type { case "Include": - from, to, trailer = r.Parent, m.model.Ref(r, "addition"), " /* «include» */" + from, to, verb = r.Parent, m.model.Ref(r, "addition"), "includes" if to != nil && m.written(to) { - if cat, _ := m.classify(to); cat != catUseCaseDef { + if cat, _ := m.classify(to); cat != catUseCase { continue } } case "Extend": - from, to, trailer = r.Parent, m.model.Ref(r, "extendedCase"), " /* "+m.extendComment(r)+" */" + 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, trailer = clients[0], suppliers[0], " /* «Refine» */" + from, to, verb = clients[0], suppliers[0], "refines" } if from == nil || to == nil || !m.written(from) || !m.written(to) { continue } - m.usageConnection(r, m.nameOf(r), from, to, trailer) + name := m.nameOf(r) + if name == "" { + name = spoken(writeName(m.nameFor(from))) + " " + verb + " " + spoken(writeName(m.nameFor(to))) + } + if c := m.usageConnection(r, name, from, to); c != nil && r.Type == "Extend" && m.extensionDetail(r) != "" { + c.doc = m.extendComment(r) + } } } @@ -334,51 +383,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 { - if link.block { - m.connectActor(link) - continue - } - base := m.nameOf(link.end) - if base == "" { - base = lowerFirst(link.actor.Name) + if !link.block { + m.actors[link.assoc] = link } - 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) }) m.connectActor(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" - } - m.add(end, verdictFor(note), target, note) - } -} - -// 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 @@ -426,14 +443,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) { @@ -444,7 +456,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 { @@ -456,22 +468,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" } - 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) + ";") if conn := m.conns[inc]; conn != nil { m.wroteEdgeAlso(inc, conn.host, "connection", nil, conn.name) } - m.add(inc, Mapped, m.v2Name(inc), "") + m.add(inc, verdict, m.qualified(m.segments(inc.Parent)), note) m.stereotypeComments(inc) } diff --git a/internal/translate/migrate/usecase_internal_test.go b/internal/translate/migrate/usecase_internal_test.go index 08e982f419..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{"connection 'buy to browse' connect 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{"connection 'buy to browse' connect 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..fb868b1ced 100644 --- a/internal/translate/migrate/views.go +++ b/internal/translate/migrate/views.go @@ -601,7 +601,7 @@ func (m *migration) prepareConcerns() { } else { cat, _ := m.classify(owner) info.homed = (cat == catPartDef || cat == catOccurrenceDef || cat == catActionDef || cat == catRequirementDef || - cat == catUseCaseDef || cat == catView || cat == catViewpoint) && m.written(owner) + cat == catUseCase || cat == catActor || cat == catView || cat == catViewpoint) && m.written(owner) } } if info.homed { diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index e9e3585263..2dd312e462 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -159,7 +159,7 @@ 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;", diff --git a/tests/migrate/profile_test.go b/tests/migrate/profile_test.go index 20269c5d84..84713b58ca 100644 --- a/tests/migrate/profile_test.go +++ b/tests/migrate/profile_test.go @@ -77,8 +77,8 @@ func TestInheritedStereotypeSemanticsFollowEveryGeneral(t *testing.T) { 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, "connection 'spin to speed Requirement' connect spin to 'speed Requirement' {") - wantLine(t, r.Notation, "/* «Refine» */") + 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 {") diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index e4e7274384..a36d4beb35 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1385,7 +1385,7 @@ 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, "connection 'ref' connect thing to req; /* «Refine» */") + wantLine(t, r.Notation, "connection 'ref' connect thing to req;") wantLine(t, r.Notation, "allocation def alloc {") wantLine(t, r.Notation, "end :>> source : Thing;") wantLine(t, r.Notation, "end :>> target : Piece;") @@ -1678,21 +1678,25 @@ func TestItemFlowWithEndsStandingForBothIsNotMigrated(t *testing.T) { wantClean(t, "t.sysml", r) } -// 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. +// 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 : Driver;", "part bank : Bank;", "use case 'park Car' : 'Park Car';", "use case 'pay Fee' : 'Pay Fee';"} { + 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';") + 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;") + wantLine(t, v.Notation, "connection drives : Drives connect Driver to vehicle;") wantNoLine(t, v.Notation, "connection Drives ") } @@ -1709,8 +1713,9 @@ const refineEndsApplications = ` func TestRefineWithSeveralEndsIsAConnectionPerPair(t *testing.T) { r := migrateDocument(t, refineEndsModel+` `, refineEndsApplications) - wantLine(t, r.Notation, "connection 'alpha to req' connect alpha to req; /* «Refine» */") - wantLine(t, r.Notation, "connection 'beta to req' connect beta to req; /* «Refine» */") + 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) @@ -1718,8 +1723,8 @@ func TestRefineWithSeveralEndsIsAConnectionPerPair(t *testing.T) { // 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; /* «Refine» */") - wantLine(t, r.Notation, "connection 'Link 2' connect beta to req; /* «Refine» */") + wantLine(t, r.Notation, "connection Link connect Alpha to req;") + wantLine(t, r.Notation, "connection 'Link 2' connect Beta to req;") wantNoLine(t, r.Notation, "connection 'Link 2 2'") } @@ -1729,8 +1734,8 @@ func TestConnectionNameYieldsToAPackageMember(t *testing.T) { r := migrateDocument(t, refineEndsModel+` `, refineEndsApplications) - wantLine(t, r.Notation, "use case def Link;") - wantLine(t, r.Notation, "connection 'Link 2' connect alpha to req; /* «Refine» */") + 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) @@ -1744,7 +1749,7 @@ func TestConnectionNameYieldsToAPackageMember(t *testing.T) { `, "") wantLine(t, r.Notation, "connection def drives {") - wantLine(t, r.Notation, "connection 'drives 2' : drives connect driver to drive;") + wantLine(t, r.Notation, "connection 'drives 2' : drives connect Driver to Drive;") } const actorEndModel = ` @@ -1789,7 +1794,7 @@ func viewBody(t *testing.T, notation []byte, name string) string { 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;"} { + 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) } diff --git a/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt b/tests/migrate/testdata/xmi/diagram_edges.golden.report.txt index 4852c80c95..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' +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 6367eb096d..98aa18053f 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml @@ -126,22 +126,18 @@ 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 'drive Vehicle' : 'Drive Vehicle'; - use case 'start Engine' : 'Start Engine'; - connection 'drive Vehicle to start Engine' connect 'drive Vehicle' to 'start Engine'; /* «include» */ - use case 'stop Engine' : 'Stop Engine'; - connection 'drive Vehicle to stop Engine' connect 'drive Vehicle' to 'stop Engine'; /* «include» */ + 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 'drive Vehicle to start Engine'; + expose 'Drive Vehicle'; + expose 'Start Engine'; + expose 'Drive Vehicle includes Start Engine'; render Views::asInterconnectionDiagram; } } 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 ea47dc534f..79560240bb 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.report.txt @@ -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' +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 50fc4b5bac..383a5a5190 100644 --- a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml +++ b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml @@ -158,26 +158,22 @@ 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 'drive Vehicle' : 'Drive Vehicle'; - use case 'start Engine' : 'Start Engine'; - connection 'drive Vehicle to start Engine' connect 'drive Vehicle' to 'start Engine'; /* «include» */ - use case 'stop Engine' : 'Stop Engine'; - connection 'drive Vehicle to stop Engine' connect 'drive Vehicle' to 'stop Engine'; /* «include» */ + 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 'drive Vehicle to start Engine'; + expose 'Drive Vehicle'; + expose '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; } - metadata DiagramLayout::Route about 'drive Vehicle to start Engine' { points = (200, 30, 110, 30); } + 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; } + metadata DiagramLayout::Route about 'Drive Vehicle includes Start Engine' { points = (200, 30, 110, 30); } render Views::asInterconnectionDiagram; } } diff --git a/tests/migrate/testdata/xmi/diagrams.golden.report.txt b/tests/migrate/testdata/xmi/diagrams.golden.report.txt index 4928468b9c..c245d1fd57 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.report.txt +++ b/tests/migrate/testdata/xmi/diagrams.golden.report.txt @@ -1,8 +1,8 @@ # SysML v1 to v2 migration report: diagrams.xmi # exported by Example UML Tool -# migrated 62 element(s): 47 mapped, 15 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) +# migrated 62 element(s): 46 mapped, 16 approximated, 0 unmapped (2 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) -## approximated (15) +## approximated (16) OpaqueAction Behavior::Drive::start engine _act_start -> 'start engine' (the JavaScript body is translated to v2; running is declared nowhere, so it is declared a local attribute of the action: a script's assignment to an undeclared name creates it) Diagram Behavior::Drive::start engine _diag_act_node -> Behavior::Drive::'start engine 2' (a SysML Activity Diagram written as a view rendered asInterconnectionDiagram of ActionFlowView; the view exposes action def Behavior::Drive, whose graph the rendering draws with the 1 shown nodes and edges of it; written as start engine 2 since a member of its owner is also named start engine) 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) @@ -15,11 +15,12 @@ Operation Structure::Engine::Halt _op_halt -> Structure::Engine::Halt (the JavaS 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) +Actor Usage::Driver _actor_driver -> Usage::Driver (a UML actor is written as a part usage) -## mapped (47) +## mapped (46) Package Behavior _pkg_behavior -> Behavior Activity Behavior::Drive _act_drive -> Behavior::Drive ActivityFinalNode Behavior::Drive:: _final -> final @@ -63,9 +64,8 @@ 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) diff --git a/tests/migrate/testdata/xmi/diagrams.golden.sysml b/tests/migrate/testdata/xmi/diagrams.golden.sysml index d56e8d7cd9..b510309b8c 100644 --- a/tests/migrate/testdata/xmi/diagrams.golden.sysml +++ b/tests/migrate/testdata/xmi/diagrams.golden.sysml @@ -113,18 +113,13 @@ package Behavior { } package Usage { comment /* The driver drives. */ - part def Driver; - use case def 'Drive Vehicle' { - subject; - actor driver : Driver; - } - part driver : Driver; - use case 'drive Vehicle' : 'Drive Vehicle'; - connection 'driver to drive Vehicle' connect driver to 'drive Vehicle'; + part Driver; + use case 'Drive Vehicle'; + connection 'Driver to Drive Vehicle' connect Driver to 'Drive Vehicle'; view Driving { - expose driver; - expose 'drive Vehicle'; - expose 'driver to drive Vehicle'; + expose Driver; + expose 'Drive Vehicle'; + expose 'Driver to Drive Vehicle'; render Views::asInterconnectionDiagram; } } diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt b/tests/migrate/testdata/xmi/parking_usecases.golden.report.txt index 6a898a2290..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 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 -> 'Use Cases'::'wait For Space to 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) +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 8bc1a000f0..c1e1088f83 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml @@ -1,20 +1,17 @@ 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 driver : Driver; - part attendant : Attendant; - part bank : 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'; + 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; @@ -25,53 +22,48 @@ 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'; - 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'; } - use case 'park Car' : 'Park Car'; - use case 'pay Fee' : 'Pay Fee'; - connection 'park Car to pay Fee' connect 'park Car' to 'pay Fee'; /* «include» */ - use case 'authorise Payment' : 'Authorise Payment'; - connection authorise connect 'pay Fee' to 'authorise Payment'; /* «include» */ - use case 'wait For Space' : 'Wait For Space'; - connection 'wait For Space to park Car' connect 'wait For Space' to 'park Car'; /* extends at extension point(s) 'garage full' when {English} no bay is free */ + connection 'Park Car includes Pay Fee' connect 'Park Car' to 'Pay Fee'; + connection authorise connect 'Pay Fee' to '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'; +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 66a26cf722..19e68b95d2 100644 --- a/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt +++ b/tests/migrate/testdata/xmi/profile_inheritance.golden.report.txt @@ -3,7 +3,7 @@ # 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 (6) -«Detailed By» Abstraction System:: _refine -> System::'spin to 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 refine is kept as a comment) +«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) diff --git a/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml index 485b994083..032c86676a 100644 --- a/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml +++ b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml @@ -77,8 +77,7 @@ package System { } action spin : Spin; requirement 'speed Requirement' : 'Speed Requirement'; - connection 'spin to speed Requirement' connect spin to 'speed Requirement' { - /* «Refine» */ + 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/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..64651207c6 100644 --- a/tests/migrate/testdata/xmi/report_views.golden.sysml +++ b/tests/migrate/testdata/xmi/report_views.golden.sysml @@ -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/vehicle.golden.report.txt b/tests/migrate/testdata/xmi/vehicle.golden.report.txt index efd9c0246b..9aaa3e3123 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.report.txt +++ b/tests/migrate/testdata/xmi/vehicle.golden.report.txt @@ -9,11 +9,11 @@ ## 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 to 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 refine is kept as a comment) +«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) diff --git a/tests/migrate/testdata/xmi/vehicle.golden.sysml b/tests/migrate/testdata/xmi/vehicle.golden.sysml index 1cca68861a..924a2ac96a 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; @@ -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,9 +101,8 @@ package 'Vehicle Design' { attribute :>> color = 'Vehicle Design'::'Value Types'::Color::blue; } action def Drive; - part driver : Driver; part vehicle : Vehicle; - connection drives : Drives connect driver to vehicle; + connection drives : Drives connect Driver to vehicle; action drive : Drive; } package RequirementsModel { @@ -144,7 +143,7 @@ package RequirementsModel { } package 'Empty Package'; metadata MigrationMetadata::LibraryNameAvoided about RequirementsModel { sourceName = "Requirements"; } -connection 'drive to speed Requirement' connect 'Vehicle Design'::drive to RequirementsModel::'speed Requirement'; /* «Refine» */ +connection 'Drive refines Speed Requirement' connect 'Vehicle Design'::drive to RequirementsModel::'speed Requirement'; view 'Vehicle BDD' { render Views::asTextualNotation; } From 4f1b6e6ffb87261de98daff9cc8fe0e24974817d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:02:51 +0000 Subject: [PATCH 06/12] fix(migrate): named relationship connections keep their kind as doc; actor and use case instances are reported, not individual usages; Classifier filters list use case usages Co-Authored-By: jason.han --- .../use-case-connections.changed.md | 2 +- docs/reference/sysml-v1-migration.md | 5 ++-- internal/translate/migrate/classify.go | 2 +- internal/translate/migrate/migrate.go | 6 +--- internal/translate/migrate/typefilter.go | 15 +++++----- internal/translate/migrate/usecase.go | 28 +++++++++++++++---- tests/migrate/relations_test.go | 11 +++++--- .../xmi/metaclass_tables.golden.report.txt | 8 +++--- .../xmi/metaclass_tables.golden.sysml | 8 +++--- .../xmi/parking_usecases.golden.sysml | 4 ++- tests/migrate/values_test.go | 12 ++++++++ 11 files changed, 67 insertions(+), 34 deletions(-) diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md index fc7c4f9766..44909c4365 100644 --- a/changes/unreleased/use-case-connections.changed.md +++ b/changes/unreleased/use-case-connections.changed.md @@ -1 +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; an extend's extension points and condition are the connection's `doc`. 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. +- 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. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 89123ace52..f1c02b9576 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -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** | @@ -415,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, «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); several clients or suppliers are several dependencies, one per pair, and the view exposes each | +| 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; the interaction step `action x …` is an already named member | +| 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 diff --git a/internal/translate/migrate/classify.go b/internal/translate/migrate/classify.go index 8ad1374ee7..ff8449dc3f 100644 --- a/internal/translate/migrate/classify.go +++ b/internal/translate/migrate/classify.go @@ -671,7 +671,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: 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/migrate.go b/internal/translate/migrate/migrate.go index 03d5a3762c..121020df33 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -3626,11 +3626,7 @@ func (m *migration) dependencyPair(d *sysmlv1.Element, pl *placement, name strin if has(d, "Refine") { conn := m.pairConn(d, client, supplier) if conn == nil { - connName := name - if m.nameOf(d) == "" { - connName = spoken(writeName(m.nameFor(client))) + " refines " + spoken(writeName(m.nameFor(supplier))) - } - conn = m.usageConnection(d, connName, client, supplier) + conn = m.kindConnection(d, name, client, supplier, "refines") } if conn != nil { m.wroteEdgeAlso(d, conn.host, "connection", nil, conn.name) diff --git a/internal/translate/migrate/typefilter.go b/internal/translate/migrate/typefilter.go index 1edc8af54d..8516d41ab3 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -111,23 +111,24 @@ func occurrenceAwareTypes(types []string, include, exclude, note string) v2Types 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. -var classifierTypes = []string{typeDefinition, typeViewUsage, typeViewpointUsage} +// to: a definition, the view or viewpoint usage a «View» or «Viewpoint» class +// becomes, or the use case usage a use case becomes. An actor's part usage is +// left out: by type, a query cannot tell it from the part a property becomes. +var classifierTypes = []string{typeDefinition, typeViewUsage, typeViewpointUsage, typeUseCaseUsage} // 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} +var namespaceTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeUseCaseUsage, typeStateUsage} // packageableTypes are what the elements a package can own migrate to: the // classifiers, packages, and the dependencies of every stereotype. -var packageableTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, +var packageableTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeUseCaseUsage, typeDependency, typeSatisfyUsage, typeAllocationUsage} // Notes on what the broad UML metaclasses list once migrated. const ( - noteDiagramViews = "the views diagrams became are listed too" - noteClassifierExtra = "the views diagrams became and the action defs operations became are listed too" + noteDiagramViews = "the views diagrams became are listed too; actors, part usages once migrated, are not" + noteClassifierExtra = "the views diagrams became and the action defs operations became are listed too; actors, part usages once migrated, are not" elementTypeSubject = "the element type " ) diff --git a/internal/translate/migrate/usecase.go b/internal/translate/migrate/usecase.go index d488b09903..5343b4ca6f 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -294,16 +294,34 @@ func (m *migration) planConnections(rels []*sysmlv1.Element) { if from == nil || to == nil || !m.written(from) || !m.written(to) { continue } - name := m.nameOf(r) - if name == "" { - name = spoken(writeName(m.nameFor(from))) + " " + verb + " " + spoken(writeName(m.nameFor(to))) + c := m.kindConnection(r, m.nameOf(r), from, to, verb) + if c == nil { + continue } - if c := m.usageConnection(r, name, from, to); c != nil && r.Type == "Extend" && m.extensionDetail(r) != "" { - c.doc = m.extendComment(r) + 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 { diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index a36d4beb35..568da688b3 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -1385,7 +1385,8 @@ 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, "connection 'ref' connect thing to 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;") @@ -1723,8 +1724,10 @@ func TestRefineWithSeveralEndsIsAConnectionPerPair(t *testing.T) { // 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, "connection 'Link 2' connect Beta to req;") + 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'") } @@ -1735,7 +1738,7 @@ func TestConnectionNameYieldsToAPackageMember(t *testing.T) { `, refineEndsApplications) wantLine(t, r.Notation, "use case Link;") - wantLine(t, r.Notation, "connection 'Link 2' connect Alpha to req;") + 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) diff --git a/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt b/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt index e1c602ffff..4bd732dc2b 100644 --- a/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt +++ b/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt @@ -3,11 +3,11 @@ # migrated 37 element(s): 32 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## approximated (5) -«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) +«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; actors, part usages once migrated, are not) «DiagramTable» Diagram Tables::Diagrams _tbl_diagrams -> part def Tables::'Diagrams Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Diagrams Rows'; views a «View» class became are listed with the diagrams' views) -«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) -«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) -«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) +«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; actors, part usages once migrated, are not; transitions and structured activity nodes are not) +«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; actors, part usages once migrated, are not; instances of value types, written as attributes, are not) +«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; actors, part usages once migrated, are not) ## mapped (32) Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml b/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml index 98bd2bb2dc..6ee9be849e 100644 --- a/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml +++ b/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml @@ -46,7 +46,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "Dependency", "SatisfyRequirementUsage", "AllocationUsage")), + type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage", "Dependency", "SatisfyRequirementUsage", "AllocationUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -71,7 +71,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "StateUsage")), + type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage", "StateUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -96,7 +96,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Definition", "ViewUsage", "ViewpointUsage")), + type = ("Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -121,7 +121,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Definition", "ViewUsage", "ViewpointUsage")), + type = ("Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage")), property = "qualifiedName", direction = "ascending", missing = "last", diff --git a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml index c1e1088f83..560c732b33 100644 --- a/tests/migrate/testdata/xmi/parking_usecases.golden.sysml +++ b/tests/migrate/testdata/xmi/parking_usecases.golden.sysml @@ -57,7 +57,9 @@ package 'Use Cases' { 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'; + 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 */ } 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) +} From 66ea4c97b2e5201a7cb66945f313478bb25616e0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:31:41 +0000 Subject: [PATCH 07/12] fix(migrate): classifier tables list actors and use cases by name; census cites actorLink A generic table over Classifier, Type, Namespace or PackageableElement no longer selects UseCaseUsage by type, which also matched use-case-typed properties and include usages; it names the written actor and use case usages (Named intersected with the scope) and unions them with the type rows. Actor and UseCase tables select the same way. The transformation census baseline cites actorLink and connectActor for the actor mappings, scoped to uml:Actor, with the fixture counts remeasured. Co-Authored-By: jason.han --- ...sml-v1-transformation-census-baseline.json | 52 +++-- .../project/sysml-v1-transformation-census.md | 30 +-- docs/reference/sysml-v1-migration.md | 2 +- internal/translate/migrate/tables.go | 2 +- internal/translate/migrate/typefilter.go | 96 ++++++--- tests/migrate/classifier_usages_test.go | 33 ++++ tests/migrate/migrate_test.go | 1 + .../xmi/classifier_usages.golden.report.txt | 29 +++ .../xmi/classifier_usages.golden.sysml | 184 ++++++++++++++++++ .../testdata/xmi/classifier_usages.xmi | 114 +++++++++++ .../xmi/metaclass_tables.golden.report.txt | 8 +- .../xmi/metaclass_tables.golden.sysml | 8 +- 12 files changed, 489 insertions(+), 70 deletions(-) create mode 100644 tests/migrate/classifier_usages_test.go create mode 100644 tests/migrate/testdata/xmi/classifier_usages.golden.report.txt create mode 100644 tests/migrate/testdata/xmi/classifier_usages.golden.sysml create mode 100644 tests/migrate/testdata/xmi/classifier_usages.xmi diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index cf6bde5602..ea7fc80ec2 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": 102 } ], "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": 489 }, "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 f1c02b9576..8ae8bc40c0 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -610,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`, «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; 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/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 8516d41ab3..ea3b528cbe 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 } @@ -111,24 +117,27 @@ func occurrenceAwareTypes(types []string, include, exclude, note string) v2Types var behaviorTypes = []string{typeActionDef, typeCalcDef, typeStateDef, typeVerificationDef} // classifierTypes are what a UML type, which is always a classifier, migrates -// to: a definition, the view or viewpoint usage a «View» or «Viewpoint» class -// becomes, or the use case usage a use case becomes. An actor's part usage is -// left out: by type, a query cannot tell it from the part a property becomes. -var classifierTypes = []string{typeDefinition, typeViewUsage, typeViewpointUsage, typeUseCaseUsage} +// 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} // 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, typeUseCaseUsage, typeStateUsage} +var namespaceTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeStateUsage} // packageableTypes are what the elements a package can own migrate to: the // classifiers, packages, and the dependencies of every stereotype. -var packageableTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeUseCaseUsage, +var packageableTypes = []string{typePackage, typeDefinition, typeViewUsage, typeViewpointUsage, typeDependency, typeSatisfyUsage, typeAllocationUsage} // Notes on what the broad UML metaclasses list once migrated. const ( - noteDiagramViews = "the views diagrams became are listed too; actors, part usages once migrated, are not" - noteClassifierExtra = "the views diagrams became and the action defs operations became are listed too; actors, part usages once migrated, are not" + noteDiagramViews = "the views diagrams became are listed too" + noteClassifierExtra = "the views diagrams became and the action defs operations became are listed too" elementTypeSubject = "the element type " ) @@ -137,17 +146,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}, + "Type": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, + "Classifier": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, "Class": occurrenceAwareTypes(classTypes, typeOccurrenceDef, typeItemDef, ""), "Component": occurrenceAwareTypes([]string{typePartDef}, typeOccurrenceDef, typeItemDef, "a component is a part def once migrated, as a block is"), - "Actor": {types: []string{typePartUsage}, note: "an actor is a part usage once migrated"}, + "Actor": {usages: []category{catActor}}, "Behavior": {types: behaviorTypes}, "Activity": {types: []string{typeActionDef, typeCalcDef}}, "OpaqueBehavior": {types: []string{typeActionDef, typeCalcDef}}, @@ -155,7 +164,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{typeUseCaseUsage}}, + "UseCase": {usages: []category{catUseCase}}, "DataType": {types: []string{typeAttributeDef, typeEnumDef}}, "PrimitiveType": {types: []string{typeAttributeDef}}, "Enumeration": {types: []string{typeEnumDef}}, @@ -245,7 +254,7 @@ func (m *migration) typeFilter(ref sysmlv1.ElementRef) typeFilter { if doc, name, ok := standardHref(e.Href); ok { switch { case doc == "UML": - return metaclassFilter(name) + return m.nameUsages(metaclassFilter(name)) case stereotypeTypes[name].types != nil: return fromTypes("«"+name+"»", stereotypeTypes[name]) default: @@ -348,34 +357,69 @@ 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 { + 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 len(f.types) == 0 { + return usages + } rows := whereType(src, f.types...) - if f.excluding == nil { + 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 { 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)) + 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...) @@ -384,7 +428,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/tests/migrate/classifier_usages_test.go b/tests/migrate/classifier_usages_test.go new file mode 100644 index 0000000000..473cc30963 --- /dev/null +++ b/tests/migrate/classifier_usages_test.go @@ -0,0 +1,33 @@ +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") +} diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index 2dd312e462..06a281bed8 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -382,6 +382,7 @@ var constructFixtures = []string{ "stub_actions", "tables", "metaclass_tables", + "classifier_usages", "documents", "figures", "collectors", 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/metaclass_tables.golden.report.txt b/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt index 4bd732dc2b..e1c602ffff 100644 --- a/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt +++ b/tests/migrate/testdata/xmi/metaclass_tables.golden.report.txt @@ -3,11 +3,11 @@ # migrated 37 element(s): 32 mapped, 5 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to) ## approximated (5) -«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; actors, part usages once migrated, are not) +«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) «DiagramTable» Diagram Tables::Diagrams _tbl_diagrams -> part def Tables::'Diagrams Document' (the «DiagramTable» is written as a Document holding a Table over the query 'Diagrams Rows'; views a «View» class became are listed with the diagrams' views) -«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; actors, part usages once migrated, are not; transitions and structured activity nodes are not) -«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; actors, part usages once migrated, are not; instances of value types, written as attributes, are not) -«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; actors, part usages once migrated, are not) +«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) +«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) +«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) ## mapped (32) Model Model _m (the root model's members are written at the top level) diff --git a/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml b/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml index 6ee9be849e..98bd2bb2dc 100644 --- a/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml +++ b/tests/migrate/testdata/xmi/metaclass_tables.golden.sysml @@ -46,7 +46,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage", "Dependency", "SatisfyRequirementUsage", "AllocationUsage")), + type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "Dependency", "SatisfyRequirementUsage", "AllocationUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -71,7 +71,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage", "StateUsage")), + type = ("Package", "Definition", "ViewUsage", "ViewpointUsage", "StateUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -96,7 +96,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage")), + type = ("Definition", "ViewUsage", "ViewpointUsage")), property = "qualifiedName", direction = "ascending", missing = "last", @@ -121,7 +121,7 @@ package Tables { source = DocumentQueries::WhereType( source = DocumentQueries::Descendants( source = DocumentQueries::Named(qualifiedName = ("Plant"))), - type = ("Definition", "ViewUsage", "ViewpointUsage", "UseCaseUsage")), + type = ("Definition", "ViewUsage", "ViewpointUsage")), property = "qualifiedName", direction = "ascending", missing = "last", From f64be24974b5da86cc08d6af675229336ae89d54 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:41:28 +0000 Subject: [PATCH 08/12] fix(migrate): an Actor or UseCase table over a model without them lists no rows typeFilter.query only calls Named when a usage is written; with none and no type to select, the rows are the scope less itself, since Named takes at least one name. The empty_classifier_tables fixture pins it. Co-Authored-By: jason.han --- ...sml-v1-transformation-census-baseline.json | 4 +- internal/translate/migrate/typefilter.go | 30 ++++--- tests/migrate/classifier_usages_test.go | 10 +++ tests/migrate/migrate_test.go | 1 + .../empty_classifier_tables.golden.report.txt | 19 ++++ .../xmi/empty_classifier_tables.golden.sysml | 67 +++++++++++++++ .../testdata/xmi/empty_classifier_tables.xmi | 86 +++++++++++++++++++ 7 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 tests/migrate/testdata/xmi/empty_classifier_tables.golden.report.txt create mode 100644 tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml create mode 100644 tests/migrate/testdata/xmi/empty_classifier_tables.xmi diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index ea7fc80ec2..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": 102 + "files": 103 } ], "counts": { @@ -140,7 +140,7 @@ }, "uml:Class": { "pssm": 311, - "fixtures": 489 + "fixtures": 490 }, "uml:ClearAssociationAction": { "pssm": 0, diff --git a/internal/translate/migrate/typefilter.go b/internal/translate/migrate/typefilter.go index ea3b528cbe..812f44ea0f 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -393,24 +393,32 @@ func fromTypes(label string, t v2Types) typeFilter { // 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 { + 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 qcall("Except", qarg1("source", src), qarg1("exclude", src)) + } + return rows + } 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 len(f.types) == 0 { + if !typed { return usages } - 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 { - return rows - } return qcall("Union", qarg1("source", rows), qarg1("other", usages)) } diff --git a/tests/migrate/classifier_usages_test.go b/tests/migrate/classifier_usages_test.go index 473cc30963..2ad0e72d17 100644 --- a/tests/migrate/classifier_usages_test.go +++ b/tests/migrate/classifier_usages_test.go @@ -31,3 +31,13 @@ func TestClassifierTablesListActorAndUseCaseUsages(t *testing.T) { 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/migrate_test.go b/tests/migrate/migrate_test.go index 06a281bed8..e32b80ee87 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -383,6 +383,7 @@ var constructFixtures = []string{ "tables", "metaclass_tables", "classifier_usages", + "empty_classifier_tables", "documents", "figures", "collectors", 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..7f49172b5d --- /dev/null +++ b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml @@ -0,0 +1,67 @@ +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 = DocumentQueries::Except( + source = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))), + exclude = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations")))), + 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 = DocumentQueries::Except( + source = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations"))), + exclude = DocumentQueries::Descendants( + source = DocumentQueries::Named(qualifiedName = ("Operations")))), + 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 + + From da5dd3e447f86ac131d84ee6012fd46c70f6b70d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:55:21 +0000 Subject: [PATCH 09/12] fix(migrate): an empty Actor or UseCase table lists the empty sequence rather than walking its scope twice Co-Authored-By: jason.han --- internal/translate/migrate/query.go | 4 ++++ internal/translate/migrate/typefilter.go | 2 +- .../xmi/empty_classifier_tables.golden.sysml | 22 ++----------------- 3 files changed, 7 insertions(+), 21 deletions(-) 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/typefilter.go b/internal/translate/migrate/typefilter.go index 812f44ea0f..e022110825 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -407,7 +407,7 @@ func (f typeFilter) query(src qx) qx { if len(f.usages) == 0 { if !typed { // Named takes at least one name; no usage written means no row. - return qcall("Except", qarg1("source", src), qarg1("exclude", src)) + return qempty() } return rows } diff --git a/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml index 7f49172b5d..6faf2e2e32 100644 --- a/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml +++ b/tests/migrate/testdata/xmi/empty_classifier_tables.golden.sysml @@ -20,16 +20,7 @@ package Tables { } calc def 'Actors Rows' :> DocumentQueries::Query { DocumentQueries::Project( - source = DocumentQueries::OrderBy( - source = DocumentQueries::Except( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Operations"))), - exclude = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Operations")))), - property = "qualifiedName", - direction = "ascending", - missing = "last", - multiple = "first"), + source = DocumentQueries::OrderBy(source = (), property = "qualifiedName", direction = "ascending", missing = "last", multiple = "first"), properties = ("qualifiedName")) } part def 'Actors Document' :> DocumentQueries::Document { @@ -45,16 +36,7 @@ package Tables { } calc def 'Use Cases Rows' :> DocumentQueries::Query { DocumentQueries::Project( - source = DocumentQueries::OrderBy( - source = DocumentQueries::Except( - source = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Operations"))), - exclude = DocumentQueries::Descendants( - source = DocumentQueries::Named(qualifiedName = ("Operations")))), - property = "qualifiedName", - direction = "ascending", - missing = "last", - multiple = "first"), + source = DocumentQueries::OrderBy(source = (), property = "qualifiedName", direction = "ascending", missing = "last", multiple = "first"), properties = ("qualifiedName")) } part def 'Use Cases Document' :> DocumentQueries::Document { From df6dc8876c2032243b546a9a0e2d60061043eb46 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:27:07 +0000 Subject: [PATCH 10/12] feat(migrate): a v1 requirement is a requirement usage; satisfy, verify, derive and refine join it directly Co-Authored-By: jason.han --- .../use-case-connections.changed.md | 2 +- docs/reference/sysml-v1-migration.md | 14 +-- internal/doc/queryexec/derivation_test.go | 7 ++ .../queryexec/testdata/tmt_derivation.sysml | 8 ++ .../semantic/semantics/relationship_edges.go | 15 ++- internal/translate/migrate/classify.go | 16 +-- internal/translate/migrate/migrate.go | 32 +++--- internal/translate/migrate/names.go | 2 +- internal/translate/migrate/ocllower.go | 9 +- internal/translate/migrate/realization.go | 2 +- internal/translate/migrate/typefilter.go | 27 +++-- .../migrate/typefilter_internal_test.go | 6 +- internal/translate/migrate/uniqueness.go | 2 +- internal/translate/migrate/usecase.go | 4 +- internal/translate/migrate/views.go | 2 +- tests/migrate/classify_test.go | 47 +++++++- tests/migrate/documents_test.go | 6 +- tests/migrate/migrate_test.go | 12 +- tests/migrate/profile_test.go | 16 +-- tests/migrate/relations_test.go | 22 ++-- .../testdata/xmi/collectors.golden.sysml | 22 ++-- .../testdata/xmi/diagram_edges.golden.sysml | 14 +-- .../xmi/diagram_edges.layout.golden.sysml | 14 +-- .../testdata/xmi/docgen_columns.golden.sysml | 8 +- .../testdata/xmi/documents.golden.sysml | 48 +++++--- .../testdata/xmi/org_profile.golden.sysml | 6 +- .../xmi/profile_inheritance.golden.sysml | 13 +-- .../xmi/relation_subtypes.golden.sysml | 104 +++++++++++------- .../testdata/xmi/report_views.golden.sysml | 2 +- .../xmi/stereotype_filters.golden.sysml | 4 +- .../testdata/xmi/table_homonyms.golden.sysml | 4 +- .../migrate/testdata/xmi/tables.golden.sysml | 92 +++++++++------- .../migrate/testdata/xmi/vehicle.golden.sysml | 21 ++-- 33 files changed, 367 insertions(+), 236 deletions(-) diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md index 44909c4365..013cd0b6b8 100644 --- a/changes/unreleased/use-case-connections.changed.md +++ b/changes/unreleased/use-case-connections.changed.md @@ -1 +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. +- 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. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 8ae8bc40c0..decc93f4df 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -137,7 +137,7 @@ ties each of its mapping classes to the code that carries it out (or records why | 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 | @@ -176,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» | `connection ' refines ' connect to ;` between usages of its ends (an actor's `part`, a `use case`, or a `part`, `requirement`, `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 | +| «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) | @@ -382,7 +382,7 @@ 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` | -| a use case diagram | `Views::asInterconnectionDiagram` over the actors' `part` usages, the `use case` usages, the `requirement` usages written for refined requirements and the connections written for the actor associations, includes, extends and «Refine»s it shows | +| 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 @@ -417,8 +417,8 @@ otherwise a name spelled from what it is written between, in the body it is writ | 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, «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» | +| «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 @@ -610,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; 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 | +| 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/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/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/semantic/semantics/relationship_edges.go b/internal/semantic/semantics/relationship_edges.go index 431ae2c505..90ade76279 100644 --- a/internal/semantic/semantics/relationship_edges.go +++ b/internal/semantic/semantics/relationship_edges.go @@ -209,10 +209,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/translate/migrate/classify.go b/internal/translate/migrate/classify.go index ff8449dc3f..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 @@ -72,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: @@ -123,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: @@ -551,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"): @@ -671,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, cc == catActor, cc == catUseCase: + 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/migrate.go b/internal/translate/migrate/migrate.go index 121020df33..eaadb4e0ab 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -1364,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) + "> ") } @@ -1409,7 +1409,7 @@ 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 catUseCase: @@ -2297,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", "" @@ -2572,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 || cat == catUseCase || cat == catActor { + if cat, _ := m.classify(t); cat == catView || cat == catUseCase || cat == catActor || cat == catRequirement { return " :> " } } @@ -3692,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) @@ -3707,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 } @@ -3751,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 { @@ -3772,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 @@ -3890,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 == "" { @@ -3902,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) @@ -4009,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 a6ae7da7bb..f94d32bbe9 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -354,7 +354,7 @@ func (m *migration) isUsage(e *sysmlv1.Element) bool { return true } cat, _ := m.classify(e) - return cat == catView || cat == catUseCase || cat == catActor + 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/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/typefilter.go b/internal/translate/migrate/typefilter.go index e022110825..d644cd34f6 100644 --- a/internal/translate/migrate/typefilter.go +++ b/internal/translate/migrate/typefilter.go @@ -48,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" @@ -91,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. @@ -113,6 +112,12 @@ 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} @@ -123,7 +128,7 @@ var behaviorTypes = []string{typeActionDef, typeCalcDef, typeStateDef, typeVerif // so classifierUsages lists them by name instead. var classifierTypes = []string{typeDefinition, typeViewUsage, typeViewpointUsage} -var classifierUsages = []category{catActor, catUseCase} +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. @@ -154,7 +159,7 @@ var metaclassTypes = map[string]v2Types{ "Model": {types: []string{typePackage}, note: "a model is a package once migrated"}, "Type": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, "Classifier": {types: classifierTypes, usages: classifierUsages, note: noteClassifierExtra}, - "Class": occurrenceAwareTypes(classTypes, typeOccurrenceDef, typeItemDef, ""), + "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": {usages: []category{catActor}}, "Behavior": {types: behaviorTypes}, @@ -203,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}}, @@ -255,8 +260,8 @@ func (m *migration) typeFilter(ref sysmlv1.ElementRef) typeFilter { switch { case doc == "UML": return m.nameUsages(metaclassFilter(name)) - case stereotypeTypes[name].types != nil: - return fromTypes("«"+name+"»", stereotypeTypes[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 + "»"} } @@ -283,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 + "»"} } @@ -297,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), @@ -309,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)} 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 59ad2410af..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}, diff --git a/internal/translate/migrate/usecase.go b/internal/translate/migrate/usecase.go index 5343b4ca6f..820c3ca4b0 100644 --- a/internal/translate/migrate/usecase.go +++ b/internal/translate/migrate/usecase.go @@ -65,7 +65,7 @@ func (m *migration) usageKeyword(def *sysmlv1.Element) string { return "use case" case catActor: return "part" - case catRequirementDef: + case catRequirement: return "requirement" case catActionDef: return "action" @@ -118,7 +118,7 @@ func (m *migration) defUsageOf(host, def *sysmlv1.Element, keyword string) *defU // 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 + return cat == catUseCase || cat == catActor || cat == catRequirement } // usageIn is the one usage written for def, which a view draws for it; nil for none. diff --git a/internal/translate/migrate/views.go b/internal/translate/migrate/views.go index fb868b1ced..24b760afbc 100644 --- a/internal/translate/migrate/views.go +++ b/internal/translate/migrate/views.go @@ -600,7 +600,7 @@ 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 || + info.homed = (cat == catPartDef || cat == catOccurrenceDef || cat == catActionDef || cat == catRequirement || cat == catUseCase || cat == catActor || cat == catView || cat == catViewpoint) && m.written(owner) } } 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/migrate_test.go b/tests/migrate/migrate_test.go index e32b80ee87..adb7318808 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -165,20 +165,20 @@ func TestNotationCoversTheFixture(t *testing.T) { "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;", diff --git a/tests/migrate/profile_test.go b/tests/migrate/profile_test.go index 84713b58ca..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,9 +75,9 @@ 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, "connection 'Spin refines Speed Requirement' connect spin to 'speed Requirement' {") + 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;") diff --git a/tests/migrate/relations_test.go b/tests/migrate/relations_test.go index 568da688b3..470d97942c 100644 --- a/tests/migrate/relations_test.go +++ b/tests/migrate/relations_test.go @@ -145,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} { @@ -1383,9 +1383,9 @@ 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, "connection 'ref' connect 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;") @@ -1424,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) } @@ -1714,8 +1714,8 @@ const refineEndsApplications = ` 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;") + 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") { @@ -1724,9 +1724,9 @@ func TestRefineWithSeveralEndsIsAConnectionPerPair(t *testing.T) { // 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, "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, "connection 'Link 2' connect Beta to Req {") wantLine(t, r.Notation, "doc /* Beta refines Req */") wantNoLine(t, r.Notation, "connection 'Link 2 2'") } @@ -1738,7 +1738,7 @@ func TestConnectionNameYieldsToAPackageMember(t *testing.T) { `, refineEndsApplications) wantLine(t, r.Notation, "use case Link;") - wantLine(t, r.Notation, "connection 'Link 2' connect Alpha to req {") + 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) 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.sysml b/tests/migrate/testdata/xmi/diagram_edges.golden.sysml index 98aa18053f..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'; diff --git a/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml b/tests/migrate/testdata/xmi/diagram_edges.layout.golden.sysml index 383a5a5190..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'; 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/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/profile_inheritance.golden.sysml b/tests/migrate/testdata/xmi/profile_inheritance.golden.sysml index 032c86676a..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'; } } @@ -66,8 +66,8 @@ package System { @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' { @@ -76,8 +76,7 @@ package System { @Tailoring::'Assigned To'; } action spin : Spin; - requirement 'speed Requirement' : 'Speed Requirement'; - connection 'Spin refines Speed Requirement' connect spin to 'speed Requirement' { + 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.sysml b/tests/migrate/testdata/xmi/report_views.golden.sysml index 64651207c6..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. */ } 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.sysml b/tests/migrate/testdata/xmi/vehicle.golden.sysml index 924a2ac96a..c20e8f424d 100644 --- a/tests/migrate/testdata/xmi/vehicle.golden.sysml +++ b/tests/migrate/testdata/xmi/vehicle.golden.sysml @@ -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; @@ -106,21 +106,21 @@ package 'Vehicle Design' { 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 'Speed Requirement' to 'Vehicle Design'::Engine; /* «Trace» */ @@ -135,15 +135,14 @@ package RequirementsModel { dependency 'Vehicle Design'::Wheel to 'Vehicle Design'::Engine; verification def 'Mass Test' { objective { - verify requirement : 'Mass Requirement'; + verify 'Mass Requirement'; } } - requirement 'speed Requirement' : 'Speed 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'; +connection 'Drive refines Speed Requirement' connect 'Vehicle Design'::drive to RequirementsModel::'Speed Requirement'; view 'Vehicle BDD' { render Views::asTextualNotation; } From a1cb30124122a4979b19a89f69147e4acc06ef09 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:35:54 +0000 Subject: [PATCH 11/12] fix(migrate): a named satisfy or verify subsetting a requirement usage relates its subject to it; an allocate to a requirement stays a dependency Co-Authored-By: jason.han --- .../unreleased/use-case-connections.changed.md | 2 +- internal/doc/queryexec/related_test.go | 12 ++++++------ .../semantic/semantics/relationship_edges.go | 13 ++++++++----- internal/translate/migrate/migrate.go | 2 +- tests/migrate/stereotype_metadata_test.go | 17 +++++++++++++++++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/changes/unreleased/use-case-connections.changed.md b/changes/unreleased/use-case-connections.changed.md index 013cd0b6b8..959fd2837f 100644 --- a/changes/unreleased/use-case-connections.changed.md +++ b/changes/unreleased/use-case-connections.changed.md @@ -1 +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. +- 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/internal/doc/queryexec/related_test.go b/internal/doc/queryexec/related_test.go index ae7f3e6c2b..58c33d56ea 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,9 @@ 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"}) assertRelated(t, fixture, "relayControl::relayRequirement", "satisfaction", "incoming", 1, []string{"relayHub"}) @@ -340,7 +340,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/semantic/semantics/relationship_edges.go b/internal/semantic/semantics/relationship_edges.go index 90ade76279..443a420df2 100644 --- a/internal/semantic/semantics/relationship_edges.go +++ b/internal/semantic/semantics/relationship_edges.go @@ -84,7 +84,7 @@ 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, original, subject *symbols.Symbol for _, rel := range usage.Relationships { if rel == nil || rel.Target == nil { continue @@ -95,12 +95,15 @@ func (m *Model) satisfactionEdges(sym *symbols.Symbol, kind RelationshipKind) [] } switch rel.Kind { case ast.RelSubsets: - if !usage.DeclaresRequirement { + switch { + case !usage.DeclaresRequirement: requirement = target + case target.Kind == symbols.SymbolRequirementUsage: + original = target } case ast.RelTyping: if usage.DeclaresRequirement && target.Kind == symbols.SymbolRequirementDef { - definition = target + original = target } case ast.RelSubject: subject = target @@ -119,8 +122,8 @@ 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}) + if original != nil { + out = append(out, RelationshipEdge{Source: subject, Target: original}) } return out } diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index eaadb4e0ab..8884451eaf 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -3788,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 { 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 := ` From a3ee5eb05f8eea6c07a13c5c5d1ff4e6efb8be77 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:04:49 +0000 Subject: [PATCH 12/12] fix(semantics): a declaring satisfy or verify relates its subject to every requirement it subsets or is typed by, aliases followed Co-Authored-By: jason.han --- internal/doc/queryexec/related_test.go | 7 ++++ .../testdata/tmt_relationships.sysml | 7 ++++ .../semantic/semantics/relationship_edges.go | 36 +++++++++++++------ .../semantics/relationship_edges_test.go | 7 ++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/internal/doc/queryexec/related_test.go b/internal/doc/queryexec/related_test.go index 58c33d56ea..186e808784 100644 --- a/internal/doc/queryexec/related_test.go +++ b/internal/doc/queryexec/related_test.go @@ -167,6 +167,13 @@ func TestExecuteRelatedDeclaredRequirements(t *testing.T) { // A declared requirement that subsets another is an edge target beside it. assertRelated(t, fixture, "relayHub", "satisfaction", "outgoing", 1, []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"}) 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/semantic/semantics/relationship_edges.go b/internal/semantic/semantics/relationship_edges.go index 443a420df2..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, original, 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,17 +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: + case ast.RelSubsets, ast.RelTyping: switch { case !usage.DeclaresRequirement: - requirement = target - case target.Kind == symbols.SymbolRequirementUsage: - original = target - } - case ast.RelTyping: - if usage.DeclaresRequirement && target.Kind == symbols.SymbolRequirementDef { - original = target + if rel.Kind == ast.RelSubsets { + requirement = target + } + case isRequirementKind(target.Kind): + originals = append(originals, target) } case ast.RelSubject: subject = target @@ -122,12 +126,24 @@ func (m *Model) satisfactionEdges(sym *symbols.Symbol, kind RelationshipKind) [] return nil } out := []RelationshipEdge{{Source: subject, Target: requirement}} - if original != nil { + 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 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) {