diff --git a/changes/unreleased/portable-migration-inlines-libraries.added.md b/changes/unreleased/portable-migration-inlines-libraries.added.md
new file mode 100644
index 0000000000..5808c58f01
--- /dev/null
+++ b/changes/unreleased/portable-migration-inlines-libraries.added.md
@@ -0,0 +1 @@
+- **`-portable` makes a SysML v1 migration one self-contained file.** With `-migrate sysml -portable`, the OpenSysML library packages the output refers to — `DiagramLayout`, `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries`, and whatever those refer to in turn — are appended after the model, a KerML library in its SysML spelling, so the file loads in a tool that ships only the standard library without changing what the migration writes; the report names the packages inlined. `-strict` remains the way to an output that refers to no OpenSysML library at all.
diff --git a/changes/unreleased/strict-migration-standard-library.changed.md b/changes/unreleased/strict-migration-standard-library.changed.md
new file mode 100644
index 0000000000..d7310c28ac
--- /dev/null
+++ b/changes/unreleased/strict-migration-standard-library.changed.md
@@ -0,0 +1 @@
+- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state whose else branch is guarded by the negation of the other guards, a branch probability a comment, an interval wait its midpoint, a view carries no layout, a Monte Carlo analysis loses its `Simulation::MonteCarlo` generalization, and a history pseudostate, a document (with every view and paragraph in it) or a table is refused with a report line; a layout export is still joined to the views, the report counting the geometry it omits; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged.
diff --git a/cmd/sysml/convert_test.go b/cmd/sysml/convert_test.go
index ed26632b55..4c2a7322d8 100644
--- a/cmd/sysml/convert_test.go
+++ b/cmd/sysml/convert_test.go
@@ -524,6 +524,8 @@ func TestMigrateMigratesXMI(t *testing.T) {
"migrate several models": {[]string{v1, v1, "-migrate", "sysml"}, "-migrate migrates one SysML v1 model per run"},
"id with migrate": {[]string{v1, "-migrate", "ttl", "-id", "uuid"}, "-id accompanies -convert"},
"image base URL with convert": {[]string{model, "-convert", "ttl", "-image-base-url", "https://ve.example.org"}, "-image-base-url accompanies -migrate"},
+ "portable with convert": {[]string{model, "-convert", "ttl", "-portable"}, "-portable accompanies -migrate"},
+ "portable with validate": {[]string{model, "-validate", "-portable"}, "-portable accompanies -migrate"},
"report over the model": {[]string{xmi, "-migrate", "sysml", "-o", textReport, "-migration-report", textReport}, "-migration-report and -o both name"},
"report over the model through dangling links": {[]string{xmi, "-migrate", "sysml", "-o", danglingLink(t, dir, "model-link", "shared.txt"), "-migration-report", danglingLink(t, dir, "report-link", "shared.txt")}, "-migration-report and -o both name"},
"report over the input": {[]string{xmi, "-migrate", "sysml", "-migration-report", xmi}, "names the model being migrated"},
diff --git a/cmd/sysml/features.go b/cmd/sysml/features.go
index ec9732d9c2..82137d0e1f 100644
--- a/cmd/sysml/features.go
+++ b/cmd/sysml/features.go
@@ -53,7 +53,7 @@ func boolFlags(names ...string) []featureFlag {
var (
v1Feature = &feature{
name: "v1",
- flags: flags("migrate", "migration-report", "migration-results", "layout", "image-base-url"),
+ flags: append(flags("migrate", "migration-report", "migration-results", "layout", "image-base-url"), boolFlags("portable")...),
values: map[string][]string{"from": {"xmi", "uml", "mdzip"}},
sections: []string{"Migration"},
markers: []string{"SysML v1"},
diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go
index e088b5bc86..0949e5d38e 100644
--- a/cmd/sysml/main.go
+++ b/cmd/sysml/main.go
@@ -152,6 +152,7 @@ var (
htmlMath string
htmlTheme string
strictMode bool
+ portableMode bool
disabledLints lintList
enabledLints lintList
noRecordCache bool
@@ -544,6 +545,10 @@ func runCLI() int {
fmt.Fprintln(os.Stderr, "sysml: -image-base-url accompanies -migrate of a SysML v1 model; write `sysml Model.mdzip -migrate sysml -image-base-url https://ve.example.org`")
return 2
}
+ if portableMode && migrateFormat == "" {
+ fmt.Fprintln(os.Stderr, "sysml: -portable accompanies -migrate of a SysML v1 model; write `sysml Model.mdzip -migrate sysml -portable -o Model.sysml`")
+ return 2
+ }
if message := importMisuse(); message != "" {
fmt.Fprintf(os.Stderr, "sysml: %s\n", message)
return 2
diff --git a/cmd/sysml/migrate.go b/cmd/sysml/migrate.go
index c708046109..dab269107a 100644
--- a/cmd/sysml/migrate.go
+++ b/cmd/sysml/migrate.go
@@ -27,6 +27,7 @@ func init() {
fs.StringVar(&migrationResults, "migration-results", "", "With -migrate, write the run configurations and the result snapshots the simulation tool stored for them to this JSON file, for -compare-results to read against the migrated model")
fs.StringVar(&layoutPath, "layout", "", "With -migrate, read this MTIP export (HUDS XML) and write the diagram geometry it records as DiagramLayout annotations in the migrated views")
fs.StringVar(&imageBaseURL, "image-base-url", "", "With -migrate, the http(s) URL a comment's relative
is resolved against, such as the View Editor server")
+ fs.BoolVar(&portableMode, "portable", false, "With -migrate, append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them")
})
}
@@ -139,7 +140,7 @@ func migrateInput(name string, data []byte, _ convert.Format, to convert.Format)
// options; none were given when the flag was not passed.
func migrationOptions() (convert.MigrateOptions, error) {
if layoutPath == "" {
- return convert.MigrateOptions{ImageBaseURL: imageBaseURL, Strict: strictMode}, nil
+ return convert.MigrateOptions{ImageBaseURL: imageBaseURL, Strict: strictMode, Portable: portableMode}, nil
}
data, err := os.ReadFile(layoutPath)
if err != nil {
@@ -149,7 +150,7 @@ func migrationOptions() (convert.MigrateOptions, error) {
if err != nil {
return convert.MigrateOptions{}, fmt.Errorf("%s: %w", layoutPath, err)
}
- return convert.MigrateOptions{Layout: layout, LayoutSource: layoutPath, ImageBaseURL: imageBaseURL, Strict: strictMode}, nil
+ return convert.MigrateOptions{Layout: layout, LayoutSource: layoutPath, ImageBaseURL: imageBaseURL, Strict: strictMode, Portable: portableMode}, nil
}
// writeMigrationReport writes the report to the -migration-report file (JSON when
diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go
index 51abc40e69..c5ce4c4363 100644
--- a/cmd/sysml/usage.go
+++ b/cmd/sysml/usage.go
@@ -649,7 +649,7 @@ func registerFlags(fs *flag.FlagSet) {
fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked or rounded-real-literal, comma-separated or repeated")
fs.Var(&enabledLints, "enable-lint", "Report this opt-in lint, off by default, in the model's diagnostics: rounded-real-literal, comma-separated or repeated; -disable-lint wins")
fs.BoolVar(&noRecordCache, "no-record-cache", false, "Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0")
- fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it")
+ fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it and refers to no OpenSysML library")
fs.Var(&modelChecks.constraints, "constraint", "Evaluate this constraint and exit (repeatable)")
fs.BoolVar(&modelChecks.selfCheck, "self-check", false, "Apply the SysMLValidation constraints to every reflectively classified model element and exit")
fs.Var(&modelChecks.selfCheckPackages, "self-check-package", "Apply this package's constraint defs with the SysMLValidation ones, as -self-check-package Acme::ModelingRules; implies -self-check (repeatable)")
@@ -826,6 +826,7 @@ func optionGroups() []usage.OptionGroup {
usage.Opt("migration-results", fileArg),
usage.Opt("layout", fileArg),
usage.Opt("image-base-url", ""),
+ usage.Opt("portable", ""),
usage.Opt("compare-results", fileArg),
},
}, {
diff --git a/docs/guide/03-command-line.md b/docs/guide/03-command-line.md
index a0eddcb438..073507f213 100644
--- a/docs/guide/03-command-line.md
+++ b/docs/guide/03-command-line.md
@@ -226,7 +226,7 @@ friends — reported as warnings that name the replacement, so a model that uses
still analyses cleanly. `-strict` promotes those warnings
to errors, which turns the run into a test of whether the file is conforming SysML v2. The flag
applies to `-migrate` too: a strict SysML v1 migration writes no extension notation
-at all — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict).
+at all and refers to no OpenSysML library — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict); `-portable` instead appends the OpenSysML library packages a default migration refers to, so the one file loads elsewhere unchanged.
The state machine below uses the `choice` extension so the difference is visible:
diff --git a/docs/guide/11-migrating-from-sysml-v1.md b/docs/guide/11-migrating-from-sysml-v1.md
index 7c6b6c704d..34607fe499 100644
--- a/docs/guide/11-migrating-from-sysml-v1.md
+++ b/docs/guide/11-migrating-from-sysml-v1.md
@@ -315,24 +315,39 @@ its type: `end [0..1] driver : Driver;`. From this point `%save` in the REPL, `-
LSP and the clients all take the file as they take any other; nothing remembers that it was
migrated.
-## Portable output with `-strict`
-
-Pseudostates are written through the `StateMachines` library's metadata
-spellings — `#StateMachines::junction state x;` — with `private import
-StateMachines::*;` added to each package that holds one, so a migrated model
-reads as conforming SysML v2 either way. Pass `-strict` (see
-[Strict conformance](03-command-line.md#strict-conformance)) and the migration
-still writes only notation a pinned grammar admits — the pseudostates keep
-their `StateMachines` metadata spellings in both modes:
-
-```console
-$ sysml Project.xmi -strict -migrate sysml -o Project.sysml
-$ sysml -strict -validate Project.sysml
-✓ Project.sysml: no errors
-```
-
-What a strict migration refuses is described in the reference under
-[Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict).
+## Portable output: `-portable` and `-strict`
+
+A migrated model refers to OpenSysML's own library packages where v1 has no standard v2
+form: a pseudostate is written through the `StateMachines` library's metadata spellings
+(`#StateMachines::junction state x;`, with `private import StateMachines::*;` added to each
+package that holds one), a branch probability through `Stochastic`, a view's geometry through
+`DiagramLayout`, the names the migration made up through `MigrationMetadata`. OpenSysML ships
+them, so the file loads here as it is; a tool that ships only the standard library — the pilot
+implementation, or a modeling tool built on it — reports every such reference unresolved. Two
+flags make the output portable to such a tool:
+
+- `-portable` keeps the migration as it is and appends the library packages the output refers
+ to, so the one file carries everything it needs; the report names them:
+
+ ```console
+ $ sysml Project.xmi -migrate sysml -portable -o Project.sysml
+ migration: … inlined 4 OpenSysML library package(s): DiagramLayout, MigrationMetadata, StateMachines, Stochastic
+ ```
+
+- `-strict` (see [Strict conformance](03-command-line.md#strict-conformance)) writes no
+ OpenSysML reference at all — a pseudostate as a plain state, a probability as a comment, a
+ view without geometry — at the cost of what the reference lists under
+ [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict),
+ so the output also reads as conforming SysML v2:
+
+ ```console
+ $ sysml Project.xmi -strict -migrate sysml -o Project.sysml
+ $ sysml -strict -validate Project.sysml
+ ✓ Project.sysml: no errors
+ ```
+
+Both are described in the reference under
+[Portable output with `-portable`](../reference/sysml-v1-migration.md#portable-output-with--portable).
A deferred signal is standard notation in both modes: the state gets an `item` buffer its do
action fills from an accept loop while the state is active, substates included, and its exit
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index d59b7cdff8..8459356425 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -249,6 +249,7 @@ the same member-path parser as `Project` and `OrderBy`.
| `--migration-report ` | | With `--migrate`: write the element-by-element migration report to this file, JSON when it ends in `.json`, text otherwise. Without it the one-line summary goes to stderr |
| `--migration-results ` | | With `--migrate`: write the simulation tool's run configurations (`SimulationProfile:SimulationConfig`) and the result snapshots it stored for each of them to this JSON file — the sidecar `-compare-results` reads against the migrated model. See [Comparing a migrated configuration with the tool's results](#comparing-a-migrated-configuration-with-the-tools-results) |
| `--image-base-url ` | | With `--migrate`: the absolute http(s) URL a comment's relative `
` — a path the View Editor serves, such as `/projects/.../png` — is resolved against, so the migrated document's `Image` block points at the server instead of losing the image (see [SysML v1 migration](sysml-v1-migration.md)) |
+| `--portable` | | With `--migrate`: append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them (see [Portable output with `--portable`](sysml-v1-migration.md#portable-output-with--portable)) |
| `--render ` | | Render this view of the model (every file named, loaded as one) instead of running it, in the form its `render` member states (see [Rendering a view](#rendering-a-view)) |
| `--render-all ` | | Render every declared view into the directory, one artifact per view |
| `--graphs ` | | Write the lowered graph of this action or state machine, and of every behavior it performs, as the canonical `graphs:1` JSON an external analysis engine is sent ([the `graphs:1` model form](external-engines.md#the-graphs1-model-form)) to stdout, or to `--output`. The subject is a qualified name, definition or usage; a name that is no behavior, or that nothing declares, is refused with exit status 2. Cannot be combined with checks, rendering, `--convert`, `--migrate`, `--query` or `--eval` |
diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md
index 1ac8bd2cc1..7bf43d8b11 100644
--- a/docs/reference/sysml-v1-migration.md
+++ b/docs/reference/sysml-v1-migration.md
@@ -38,7 +38,7 @@ sysml: Model.mdzip is a SysML v1 model, which is migrated, not converted: every
```
and `-migrate` refuses v2 input the same way, pointing at `-convert`. The companion flags —
-`-migration-report`, `-migration-results`, `-layout`, `-image-base-url` and `-strict` — accompany
+`-migration-report`, `-migration-results`, `-layout`, `-image-base-url`, `-portable` and `-strict` — accompany
`-migrate`.
The same migration is available over gRPC (`Migrate`, with `from_format: "xmi"`, `"uml"` or
@@ -1976,24 +1976,68 @@ Every migrated model is gated in the test suite to:
A model the reader cannot make sense of — not XMI, a zipped project container, a document
without a model — is refused with an error naming the reason rather than migrated partially.
+### Portable output with `-portable`
+
+The default migration writes what v1 has no standard v2 form for through OpenSysML's own
+library packages — `StateMachines` for a pseudostate, `Stochastic` for a branch probability,
+`DiagramLayout` for a view's geometry, `MigrationMetadata` for the names the migration made up,
+`DocumentQueries` for a document or table, `RandomFunctions` or `Simulation` for a simulation
+profile — which OpenSysML ships and a tool built on the standard library alone does not.
+`-portable` leaves the migration as it is and appends those packages to the output: every
+library package the notation refers to by qualified name, and every one those refer to in
+turn, follows the model under a comment saying so, written as the library ships it except
+that a `standard library package` becomes a `library package`, which is what a file of one's
+own may declare. A KerML library (`RandomFunctions`, `OpenSysMLMathFunctions`) is written in
+its SysML spelling, its functions as `calc def`s; one with no SysML spelling is left
+referenced, and the report says so. The report's summary names the packages inlined —
+`inlined 5 OpenSysML library package(s): DiagramLayout, DocumentQueries, MigrationMetadata,
+StateMachines, Stochastic` — and its JSON form lists them under `libraries`. The one file then
+loads in the pilot implementation, or a modeling tool built on it, on its own; OpenSysML loads
+it too, the inlined packages standing in for its own. A migration that must also read as
+standard notation, with no OpenSysML reference left in it, is `-strict`'s, below.
+
### Portable output with `-strict`
A migration under `-strict` writes only notation a pinned SysML v2 production admits, so the
-output analyses clean under [strict conformance](../guide/03-command-line.md#strict-conformance)
-and carries nothing an interchange partner could not read. The `choice`, `junction`,
-`shallowHistory` and `deepHistory` pseudostates are written
-`#StateMachines:: state x;` — qualified so a member named like the metadata cannot
-shadow it — with `private import StateMachines::*;` added to each package holding one,
-under `-strict` the same as by default, and a `deferrableTrigger` is written in the
-standard notation described under [Deferred signals](#deferred-signals) in both modes,
-its deferred signal named by the `@MigrationMetadata::DeferredEvent` annotation. What a
-strict migration still refuses is whatever has no standard v2 form at all; a transition to
-or from an unmapped vertex is refused rather than written to an undeclared name.
-
-To check strict output against the pinned pilot implementation, validate it together with the
-whole OpenSysML library directory, `.kerml` files included (`RandomFunctions`, which migrated
-Monte Carlo analyses call, is KerML); see
-[Pilot differential](../project/pilot-differential.md#the-kerml-side-of-the-bridge).
+output analyses clean under [strict conformance](../guide/03-command-line.md#strict-conformance),
+and refers to no library beyond the standard's, so a tool without OpenSysML's libraries — the
+pilot implementation, or a modeling tool built on it — loads the file on its own. What the
+default migration writes through an OpenSysML library a strict one writes otherwise, or refuses
+with a report line:
+
+- the `MigrationMetadata::SynthesizedName`, `StandIn` and `LibraryNameAvoided` markers become
+ comments (`// names the migration made up: a, b`), the report carrying the same information;
+- a `choice` or `junction` pseudostate, by default `#StateMachines:: state x;`, is a plain
+ `state x;` whose guarded transitions are the pseudostate's, reported approximated since a
+ run no longer passes through it in one step; its `else` branch, which a plain state's
+ transitions would compete with rather than fall back to, is guarded by the negation of the
+ other guards (`if not (count < 2)`), or written unguarded with a report line saying so when
+ one of them is not a v2 expression; a `shallowHistory` or `deepHistory` pseudostate,
+ which has no standard form, is refused, with the transitions into and out of it;
+- a decision node's branch probability, by default `@Stochastic::Probability`, trails the
+ succession as a comment (`then b; // probability 0.25`);
+- a duration constraint over an interval, by default a `RandomFunctions::uniform` draw, is a
+ fixed wait of the interval's midpoint when both bounds are literal numbers, refused otherwise;
+- a «Document», a «DiagramTable» or «InstanceTable», whose forms are `DocumentQueries`
+ documents, are refused, a «Document» before its content is planned, so every view and
+ paragraph in it is reported unmapped with it; a Monte Carlo analysis is written as the ordinary analysis without its
+ `Simulation::MonteCarlo` generalization, reported approximated, and a simulation configuration
+ with its run settings as a comment in place of `@Simulation::Configuration`;
+- a view carries no `DiagramLayout` geometry; a `-layout` export and the diagrams' own symbol
+ streams are still joined to the views they match, the report's layout account counting the
+ views whose geometry is omitted rather than reporting them missing.
+
+The one OpenSysML reference a strict migration keeps is the deferral encoding's
+`@MigrationMetadata::DeferredEvent` and `#MigrationMetadata::DeferredKeeper` annotations,
+described under [Deferred signals](#deferred-signals): the encoding itself is standard notation,
+and the annotations are what lets a run give a transition on the deferred signal its due; a
+tool that does not know them reads the state as it is written. A transition to or from an
+unmapped vertex is refused rather than written to an undeclared name.
+
+Strict output validates against the pinned pilot implementation with the standard library
+alone; see [Pilot differential](../project/pilot-differential.md). Default output needs the
+whole OpenSysML library directory beside it, `.kerml` files included (`RandomFunctions`, which
+migrated Monte Carlo analyses call, is KerML).
#### Deferred signals
@@ -2060,12 +2104,12 @@ transition the migration
writes takes a signal, whatever it is written to — a transition into a terminate pseudostate,
written to `done`, or into a submachine state through a connection point reference, written to
the entry point's state, counts as any other: one it refuses — into a final state of another region, a state of
-another machine, a vertex whose ends it refuses, or, under `-strict`, a pseudostate with no v2
-form such as a choice — takes none, so the deferral keeps every route it would have accepted
-by; nor does such a completion transition drop the deferral. This is the one point where the
-modes differ: the default migration writes a `choice` and the transition into it, so that
-transition wins over the deferral as any other, while `-strict` refuses both and the state
-keeps the signal. An internal transition of the
+another machine, a vertex whose ends it refuses, or, under `-strict`, a history pseudostate,
+which has no standard v2 form — takes none, so the deferral keeps every route it would have
+accepted by; nor does such a completion transition drop the deferral. This is the one point
+where the modes differ: the default migration writes the history pseudostate and the
+transition into it, so that transition wins over the deferral as any other, while `-strict`
+refuses both and the state keeps the signal. An internal transition of the
state is written as a self transition, which exits and re-enters the state where v1 stayed in
it: the exit action sends the kept occurrences to self, and the accept loop, started again by
the re-entry, keeps them again unless a transition then accepts them — so the buffer survives
diff --git a/internal/translate/migrate/activity.go b/internal/translate/migrate/activity.go
index cc7e1c3121..25a261ae1a 100644
--- a/internal/translate/migrate/activity.go
+++ b/internal/translate/migrate/activity.go
@@ -967,6 +967,11 @@ func (m *migration) waitOf(e *sysmlv1.Element) wait {
case lo == hi:
expr = lo
note = joinNotes(note, "written as a fixed wait of "+lo+" s before "+describe(e))
+ case m.strict && (lerr != nil || herr != nil):
+ return wait{dc: dc, why: noStrictDraw(lo, hi)}
+ case m.strict:
+ expr = computedLiteral((lf + hf) / 2)
+ note = joinNotes(note, "written as a fixed wait of "+expr+" s, the midpoint of ["+lo+", "+hi+"], before "+describe(e)+strictMidpointWhy)
default:
expr = "RandomFunctions::uniform(" + lo + ", " + hi + ")"
note = joinNotes(note, "written as a wait drawn uniformly over ["+lo+", "+hi+"] s before "+describe(e)+"; a tool's fixed min or max mode is a run setting, not the model's")
@@ -1101,16 +1106,33 @@ func (a *activity) decisionSuccessions(n *sysmlv1.Element, from string, outs []*
}
a.m.w.lines(guards[i].comment)
tail := ";"
- if weights != nil {
+ verdict, note := Mapped, ""
+ switch {
+ case weights == nil:
+ case a.m.strict:
+ tail = "; // probability " + weights[i]
+ verdict, note = Approximated, "its probability "+weights[i]+" is written as a comment: a strict migration names no OpenSysML library, and Stochastic::Probability is one"
+ default:
tail = " { @Stochastic::Probability { p = " + weights[i] + "; } }"
}
a.succession([]*sysmlv1.Element{e}, from, guards[i].expr, to, tail)
- if guards[i].ok {
- a.m.add(e, Mapped, a.m.edgeTarget(e), "")
+ switch {
+ case guards[i].ok:
+ a.m.add(e, verdict, a.m.edgeTarget(e), note)
+ case note != "":
+ a.m.note(e, note)
}
}
}
+// strictMidpointWhy and noStrictDraw say why a strict migration writes a wait
+// over an interval as its midpoint, or cannot.
+const strictMidpointWhy = ": a strict migration names no OpenSysML library, and a draw over the interval is RandomFunctions::uniform"
+
+func noStrictDraw(lo, hi string) string {
+ return "a wait drawn over [" + lo + ", " + hi + "] is RandomFunctions::uniform, an OpenSysML library a strict migration does not name, and the bounds are no literal numbers to take the midpoint of"
+}
+
// arbitraryChoice weights the unconditional branches of a decision equally, since
// UML leaves the branch taken among several true guards unspecified.
func (a *activity) arbitraryChoice(n *sysmlv1.Element, outs []*sysmlv1.Element, tos []string, guards []guardText, elseAt int) []string {
diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go
index 720e7d5a2b..798b3460f9 100644
--- a/internal/translate/migrate/diagrams.go
+++ b/internal/translate/migrate/diagrams.go
@@ -845,12 +845,18 @@ func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeomet
case src.export:
s.DiagramsJoined++
m.layoutJoined[rec.ID] = true
- if src.stream {
+ if src.stream && !m.strict {
s.StreamSupplemented++
}
default:
s.StreamDiagrams++
}
+ if m.strict {
+ s.GeometryOmitted++
+ // Nothing is written, so the stream supplements nothing.
+ source = m.layoutSourceName(layoutSources{export: src.export})
+ return viewGeometry{note: "layout from " + source + " omitted: a strict migration names no OpenSysML library, and DiagramLayout is one's"}
+ }
prefix := diagramLayoutPrefix
if m.shadowsLibrary("DiagramLayout", v.host) {
prefix = globalDiagramLayoutPrefix
diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go
index b93cd7e526..e706a592a2 100644
--- a/internal/translate/migrate/diagrams_internal_test.go
+++ b/internal/translate/migrate/diagrams_internal_test.go
@@ -328,7 +328,7 @@ func TestDiagramNotesDoNotNameOmittedVertices(t *testing.T) {
strict bool
anchored bool
}{
- {name: "strict", strict: true, anchored: true},
+ {name: "strict", strict: true},
{name: "non-strict", anchored: true},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -348,6 +348,13 @@ func TestDiagramNotesDoNotNameOmittedVertices(t *testing.T) {
}
r := FromModelOptions("diagrams.xmi", model, Options{Strict: tc.strict})
got := string(r.Notation)
+ if tc.strict {
+ // A strict migration writes no layout, DiagramLayout being OpenSysML's.
+ if strings.Contains(got, "DiagramLayout") {
+ t.Errorf("strict notation names DiagramLayout:\n%s", got)
+ }
+ return
+ }
anchored := strings.Contains(got, "metadata DiagramLayout::Note about")
if anchored != tc.anchored {
t.Errorf("note anchored = %t, want %t:\n%s", anchored, tc.anchored, got)
diff --git a/internal/translate/migrate/documents.go b/internal/translate/migrate/documents.go
index 8645dcd504..8a1b2b9804 100644
--- a/internal/translate/migrate/documents.go
+++ b/internal/translate/migrate/documents.go
@@ -195,6 +195,10 @@ func (m *migration) planDocument(d *sysmlv1.DocGenDocument) {
if title == "" {
title = "Document"
}
+ if m.strict {
+ m.strictDocument(d, host, title)
+ return
+ }
dp := &docPlan{d: d, host: host, refused: map[string]bool{}}
dp.root = §ionPlan{v: d.Root, title: title, names: columnNames{}}
dp.root.name = m.viewName(host, title+docSuffix)
@@ -203,6 +207,30 @@ func (m *migration) planDocument(d *sysmlv1.DocGenDocument) {
m.extras[host] = append(m.extras[host], func() { m.writeDocument(dp) })
}
+// strictDocument accounts for a document a strict migration does not write,
+// before any of its content is planned: a comment where the Document would
+// be, and an Unmapped entry for it and for every view and paragraph in it.
+func (m *migration) strictDocument(d *sysmlv1.DocGenDocument, host *sysmlv1.Element, title string) {
+ note := "its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name"
+ m.extras[host] = append(m.extras[host], func() {
+ m.w.lines(commentLines("not migrated: «Document» '" + title + "' — " + note))
+ })
+ m.report.Entries = append(m.report.Entries, *m.docEntry(d, Unmapped, "", note))
+ var walk func(v *sysmlv1.DocGenView)
+ walk = func(v *sysmlv1.DocGenView) {
+ if v.Class != d.Class {
+ m.report.Entries = append(m.report.Entries, *m.nodeEntry(v.Class, viewApplication(v.Class), Unmapped, "it belongs to «Document» '"+title+"', which is not migrated: "+note))
+ }
+ for _, p := range v.Paragraphs {
+ m.report.Entries = append(m.report.Entries, *m.nodeEntry(p.Comment, p.Application, Unmapped, "it belongs to «Document» '"+title+"', which is not migrated: "+note))
+ }
+ for _, c := range v.Children {
+ walk(c)
+ }
+ }
+ walk(d.Root)
+}
+
// nameAnchors names the anchors after their definitions, clear of every
// member name the document declares, so a chain from one resolves anywhere in it.
func (m *migration) nameAnchors(dp *docPlan) {
diff --git a/internal/translate/migrate/expression.go b/internal/translate/migrate/expression.go
index a7e61dbf61..c892b97c84 100644
--- a/internal/translate/migrate/expression.go
+++ b/internal/translate/migrate/expression.go
@@ -50,6 +50,8 @@ type treeLowering struct {
}
// feature answers a placeholder for an instance operand, else asks the scope.
+func (l *treeLowering) strictMigration() bool { return l.s.strictMigration() }
+
func (l *treeLowering) feature(path []string, write bool) (opaqueRef, *refusal) {
if ref, ok := l.leaves[path[0]]; ok && len(path) == 1 {
return ref, nil
diff --git a/internal/translate/migrate/interaction.go b/internal/translate/migrate/interaction.go
index 5347e58d93..0a47f8fc33 100644
--- a/internal/translate/migrate/interaction.go
+++ b/internal/translate/migrate/interaction.go
@@ -1623,6 +1623,11 @@ func (s *scenario) waitExpr(dc *sysmlv1.Element) (expr, note string, ok bool) {
return "", "the interval's min " + lo + " exceeds its max " + hi, false
case lo == hi:
return lo, joinNotes(note, "a fixed wait of "+lo+" s"), true
+ case m.strict && (lerr != nil || herr != nil):
+ return "", noStrictDraw(lo, hi), false
+ case m.strict:
+ mid := computedLiteral((lf + hf) / 2)
+ return mid, joinNotes(note, "a fixed wait of "+mid+" s, the midpoint of ["+lo+", "+hi+"]"+strictMidpointWhy), true
}
return "RandomFunctions::uniform(" + lo + ", " + hi + ")",
joinNotes(note, "a wait drawn uniformly over ["+lo+", "+hi+"] s; a tool's fixed min or max mode is a run setting, not the model's"), true
diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go
index 6824338ed3..5c6446ecb3 100644
--- a/internal/translate/migrate/migrate.go
+++ b/internal/translate/migrate/migrate.go
@@ -60,11 +60,16 @@ type Options struct {
// ImageBaseURL resolves a comment's relative
to the server
// serving it; "" leaves such images out.
ImageBaseURL string
- // Strict writes only notation a pinned SysML v2 production admits: a
- // construct whose only v2 form is an OpenSysML extension (a deferred
- // event, a choice, junction or history pseudostate) is reported unmapped
- // instead of written.
+ // Strict writes only notation a pinned SysML v2 production admits and
+ // refers to no OpenSysML library: a construct whose only v2 form is one
+ // (a history pseudostate, a document) is reported unmapped instead of
+ // written, one with a standard approximation (a choice, a probability)
+ // written so and reported approximated.
Strict bool
+ // Portable appends the OpenSysML library packages the output refers to,
+ // so the one file loads in a tool that ships only the standard library;
+ // what is written is otherwise unchanged. The report names the packages.
+ Portable bool
}
// Migrate reads a SysML v1 model as UML XMI, or a zip archive (such as a
@@ -270,7 +275,11 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result {
m.layoutReport()
m.extensions()
m.report.Images = m.imagesWritten
- return &Result{Notation: []byte(m.w.String()), Report: m.report, Results: m.results, Files: m.files}
+ notation := []byte(m.w.String())
+ if opts.Portable {
+ notation, m.report.Libraries = inlineLibraries(notation)
+ }
+ return &Result{Notation: notation, Report: m.report, Results: m.results, Files: m.files}
}
// unwrittenEvents reports the events whose triggers were never written: those
@@ -863,6 +872,10 @@ func (m *migration) libraryNameNotes() {
prefix = "$::"
}
for _, r := range list {
+ if m.strict {
+ m.w.line("// " + writeName(r.fresh) + " is written for " + stringLiteral(r.src) + ", a name of the standard library")
+ continue
+ }
m.w.line("metadata " + prefix + "MigrationMetadata::LibraryNameAvoided about " + writeName(r.fresh) +
" { sourceName = " + stringLiteral(r.src) + "; }")
}
diff --git a/internal/translate/migrate/montecarlo.go b/internal/translate/migrate/montecarlo.go
index 7313e2ad05..1a487a086d 100644
--- a/internal/translate/migrate/montecarlo.go
+++ b/internal/translate/migrate/montecarlo.go
@@ -221,7 +221,7 @@ func (m *migration) generalizesMonteCarlo(c *sysmlv1.Element) bool {
// monteCarloCaseOf is the analysis def written beside a block: one that generalizes
// the tool's MonteCarloAnalysis, or binds a statistic of one it inherits; nil for others.
func (m *migration) monteCarloCaseOf(block *sysmlv1.Element) *monteCarloCase {
- if block == nil || block.IsProxy() {
+ if block == nil || block.IsProxy() || m.strict {
return nil
}
if cs, ok := m.monteCarlo[block]; ok {
@@ -426,7 +426,10 @@ func (cs *monteCarloCase) statistics() []string {
// MonteCarloAnalysis is written: as the sibling analysis def.
func (m *migration) monteCarloGeneralization(block *sysmlv1.Element) string {
cs := m.monteCarloCaseOf(block)
- if cs == nil {
+ switch {
+ case cs == nil && m.strict:
+ return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is not written: its v2 form is an analysis def :> " + monteCarloLibraryCase + ", an OpenSysML library a strict migration does not name"
+ case cs == nil:
return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is not written: the classifier is written as no part def for an analysis to take as its subject"
}
return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is written as the analysis def " + writeName(cs.name) + " :> " + monteCarloLibraryCase + " beside the part def, which is its subject"
diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go
index 0127cd7d9f..4411b128e6 100644
--- a/internal/translate/migrate/names.go
+++ b/internal/translate/migrate/names.go
@@ -81,9 +81,20 @@ func (m *migration) standInNames(written []string) string {
return m.migrationMarker(standInFQN, written)
}
+// strictMarkerText words the comment a strict migration writes instead of
+// each MigrationMetadata marker, the library being OpenSysML's, not the standard's.
+var strictMarkerText = map[string]string{
+ synthesizedNameFQN: "names the migration made up",
+ standInFQN: "members standing for no source element",
+}
+
// migrationMarker is the metadata usage of the MigrationMetadata definition
-// fqn about the written names, qualified past a member shadowing the library.
+// fqn about the written names, qualified past a member shadowing the library;
+// a strict migration writes a comment instead.
func (m *migration) migrationMarker(fqn string, written []string) string {
+ if m.strict {
+ return "// " + strictMarkerText[fqn] + ": " + strings.Join(written, ", ")
+ }
prefix := ""
if m.shadowsLibrary("MigrationMetadata", m.scope) {
prefix = "$::"
diff --git a/internal/translate/migrate/opaque.go b/internal/translate/migrate/opaque.go
index 67228b7ef9..e72a4a0dac 100644
--- a/internal/translate/migrate/opaque.go
+++ b/internal/translate/migrate/opaque.go
@@ -747,6 +747,7 @@ type opaqueParser struct {
i int
d dialect
kerml bool // a lowered Expression tree may spell the KerML connectives xor and implies
+ strict bool // write the standard library's functions only, never OpenSysMLMathFunctions
sc featureResolver
locals map[string]local // names a `var`, `let` or `const` declared
assigns bool // whether `=` assigns (a statement) rather than compares
@@ -771,7 +772,16 @@ func newOpaqueParser(body string, d dialect, sc featureResolver) (*opaqueParser,
if err != nil {
return nil, err
}
- return &opaqueParser{toks: toks, d: d, sc: sc, locals: map[string]local{}, assigned: map[string]bool{}, guarded: map[string][]string{}}, nil
+ p := &opaqueParser{toks: toks, d: d, sc: sc, locals: map[string]local{}, assigned: map[string]bool{}, guarded: map[string][]string{}}
+ if s, ok := sc.(strictTeller); ok {
+ p.strict = s.strictMigration()
+ }
+ return p, nil
+}
+
+// strictTeller is a featureResolver telling whether the migration is strict.
+type strictTeller interface {
+ strictMigration() bool
}
// local is a name a declaration introduced: the scalar it holds and whether
@@ -1504,7 +1514,7 @@ func (p *opaqueParser) arithmeticOf(left translated, op string, right translated
if op == "/" && p.d == dialectJava {
switch {
case wholeScalar(left.scalar) && wholeScalar(right.scalar):
- return javaQuotient(left, right), nil
+ return javaQuotient(left, right, p.strict), nil
case !realScalar(left.scalar) && !realScalar(right.scalar):
side := left
if side.scalar != "" {
@@ -1520,7 +1530,12 @@ func (p *opaqueParser) arithmeticOf(left translated, op string, right translated
// javaQuotient writes Java's `/` over whole numbers x and y: the exact quotient
// truncated toward zero, which the extension library's quotient computes and
// reports as overflow for the one pair (the least Integer by -1) outside the range.
-func javaQuotient(x, y translated) translated {
+// A strict migration computes it from the standard remainder, x % y having
+// the sign of x: (x - x % y) / y is exact, and its floor is itself.
+func javaQuotient(x, y translated, strict bool) translated {
+ if strict {
+ return translated{expr: "RealFunctions::floor((" + x.operand() + " - " + x.operand() + " % " + y.operand() + ") / " + y.operand() + ")", scalar: "Integer", atomic: true}
+ }
return translated{expr: "OpenSysMLMathFunctions::quotient(" + x.expr + ", " + y.expr + ")", scalar: "Integer", atomic: true}
}
@@ -1816,6 +1831,9 @@ func (p *opaqueParser) mathRoundish(fn string, args []translated, arity func(int
switch fn {
case mathCeilFn:
// -floor(-x) would overflow at the least Integer; the extension library's ceiling does not.
+ if p.strict {
+ return translated{expr: "-RealFunctions::floor(-" + args[0].operand() + ")", scalar: yields, atomic: true}, nil
+ }
return translated{expr: "OpenSysMLMathFunctions::ceiling(" + args[0].expr + ")", scalar: yields, atomic: true}, nil
case mathRound:
// JavaScript and Java round a half toward +∞, where RealFunctions::round rounds it away from zero.
diff --git a/internal/translate/migrate/portable.go b/internal/translate/migrate/portable.go
new file mode 100644
index 0000000000..727d6945a0
--- /dev/null
+++ b/internal/translate/migrate/portable.go
@@ -0,0 +1,181 @@
+package migrate
+
+import (
+ "bytes"
+ "sort"
+ "strings"
+
+ "github.com/Open-MBEE/OpenSysML/internal/syntax/lexer"
+ "github.com/Open-MBEE/OpenSysML/internal/syntax/source"
+ "github.com/Open-MBEE/OpenSysML/internal/workspace/libs"
+)
+
+// openSysMLLibraryDir is the directory of the library source holding the
+// OpenSysML extension libraries, one library package per file named after it.
+const openSysMLLibraryDir = "OpenSysML Libraries/"
+
+// LibrarySummary accounts for the OpenSysML library packages a portable
+// migration appended to its output, and those it could not.
+type LibrarySummary struct {
+ // Inlined names the library packages appended, in the order appended.
+ Inlined []string `json:"inlined,omitempty"`
+ // NotInlined explains, by package, why a referenced library package was
+ // not appended, so the output still refers to it.
+ NotInlined map[string]string `json:"notInlined,omitempty"`
+}
+
+// libraryText is one OpenSysML library package as the source ships it.
+type libraryText struct {
+ name string
+ kerml bool
+ text []byte
+}
+
+// openSysMLLibraries reads the OpenSysML extension libraries from the
+// embedded library source, by package name.
+func openSysMLLibraries() map[string]libraryText {
+ src := libs.EmbeddedSource()
+ out := map[string]libraryText{}
+ for _, path := range src.List() {
+ if !strings.HasPrefix(path, openSysMLLibraryDir) {
+ continue
+ }
+ base := strings.TrimPrefix(path, openSysMLLibraryDir)
+ name, kerml := strings.TrimSuffix(base, ".sysml"), false
+ if name == base {
+ name, kerml = strings.TrimSuffix(base, ".kerml"), true
+ if name == base {
+ continue
+ }
+ }
+ text, err := src.Read(path)
+ if err != nil {
+ continue
+ }
+ out[name] = libraryText{name: name, kerml: kerml, text: text}
+ }
+ return out
+}
+
+// inlineLibraries appends to notation the OpenSysML library packages it
+// refers to, and those they refer to in turn, so the file loads with the
+// standard library alone. A KerML library is written in its SysML spelling;
+// one that has none is left referenced and accounted for.
+func inlineLibraries(notation []byte) ([]byte, *LibrarySummary) {
+ available := openSysMLLibraries()
+ summary := &LibrarySummary{}
+ seen := map[string]bool{}
+ var appended [][]byte
+ pending := referencedLibraries(notation, available)
+ for len(pending) > 0 {
+ name := pending[0]
+ pending = pending[1:]
+ if seen[name] {
+ continue
+ }
+ seen[name] = true
+ lib := available[name]
+ text := lib.text
+ if lib.kerml {
+ spelled, ok := sysmlSpelling(name, text)
+ if !ok {
+ if summary.NotInlined == nil {
+ summary.NotInlined = map[string]string{}
+ }
+ summary.NotInlined[name] = "a KerML library with no SysML spelling"
+ continue
+ }
+ text = spelled
+ }
+ text = unmarkedStandard(name, text)
+ summary.Inlined = append(summary.Inlined, name)
+ appended = append(appended, text)
+ pending = append(pending, referencedLibraries(text, available)...)
+ }
+ if len(appended) == 0 {
+ return notation, summary
+ }
+ var out bytes.Buffer
+ out.Write(notation)
+ if !bytes.HasSuffix(notation, []byte("\n")) {
+ out.WriteByte('\n')
+ }
+ out.WriteString("\n// OpenSysML library packages the model refers to, inlined so the file\n// loads with the standard library alone.\n")
+ for _, text := range appended {
+ out.WriteByte('\n')
+ out.Write(text)
+ if !bytes.HasSuffix(text, []byte("\n")) {
+ out.WriteByte('\n')
+ }
+ }
+ return out.Bytes(), summary
+}
+
+// referencedLibraries names the OpenSysML library packages notation refers
+// to, each as the first segment of a qualified name, sorted.
+func referencedLibraries(notation []byte, available map[string]libraryText) []string {
+ found := map[string]bool{}
+ lx := lexer.New(source.New("migration.sysml", notation))
+ var prev lexer.Token
+ for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() {
+ if tok.IsTrivia() {
+ continue
+ }
+ if tok.Kind == lexer.ColonColon && prev.Kind == lexer.Identifier {
+ if name := string(notation[prev.Span.Offset : prev.Span.Offset+prev.Span.Len]); available[name].text != nil {
+ found[name] = true
+ }
+ }
+ prev = tok
+ }
+ names := make([]string, 0, len(found))
+ for name := range found {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
+
+// unmarkedStandard is a library package declared `standard library package`
+// without the `standard`, which only the standard library's own files may carry.
+func unmarkedStandard(name string, text []byte) []byte {
+ lx := lexer.New(source.New(name+".sysml", text))
+ var prev lexer.Token
+ for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() {
+ if tok.IsTrivia() {
+ continue
+ }
+ if tok.Kind == lexer.Keyword && string(text[tok.Span.Offset:tok.Span.Offset+tok.Span.Len]) == "library" && prev.Kind == lexer.Keyword && string(text[prev.Span.Offset:prev.Span.Offset+prev.Span.Len]) == "standard" {
+ out := append([]byte(nil), text[:prev.Span.Offset]...)
+ return append(out, text[tok.Span.Offset:]...)
+ }
+ prev = tok
+ }
+ return text
+}
+
+// sysmlSpelling writes a KerML library in SysML notation: its functions as
+// calc defs. ok is false when a word of it is KerML's alone, as `feature` or
+// `featured by` are, so no SysML file can say what it says.
+func sysmlSpelling(name string, kerml []byte) (spelled []byte, ok bool) {
+ var out bytes.Buffer
+ lx := lexer.New(source.New(name+".kerml", kerml))
+ pos := 0
+ for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() {
+ if tok.Kind != lexer.Keyword {
+ continue
+ }
+ end := tok.Span.Offset + tok.Span.Len
+ word := string(kerml[tok.Span.Offset:end])
+ switch {
+ case word == "function":
+ out.Write(kerml[pos:tok.Span.Offset])
+ out.WriteString("calc def")
+ pos = end
+ case !source.IsKeywordIn(word, source.KindSysML):
+ return nil, false
+ }
+ }
+ out.Write(kerml[pos:])
+ return out.Bytes(), true
+}
diff --git a/internal/translate/migrate/portable_test.go b/internal/translate/migrate/portable_test.go
new file mode 100644
index 0000000000..612919d35b
--- /dev/null
+++ b/internal/translate/migrate/portable_test.go
@@ -0,0 +1,46 @@
+package migrate
+
+import (
+ "strings"
+ "testing"
+)
+
+// Inlining follows what an inlined library refers to in turn: MOSA imports
+// DocumentQueries, so both are appended.
+func TestInlineLibrariesFollowsWhatALibraryRefersTo(t *testing.T) {
+ out, s := inlineLibraries([]byte("package P { private import MOSA::*; }\n"))
+ if want := []string{"MOSA", "DocumentQueries"}; strings.Join(s.Inlined, ",") != strings.Join(want, ",") {
+ t.Errorf("inlined %v, want %v", s.Inlined, want)
+ }
+ for _, decl := range []string{"library package MOSA {", "library package DocumentQueries {"} {
+ if !strings.Contains(string(out), decl) {
+ t.Errorf("output lacks %q", decl)
+ }
+ }
+ if len(s.NotInlined) > 0 {
+ t.Errorf("not inlined: %v", s.NotInlined)
+ }
+}
+
+// A KerML library with no SysML spelling is left referenced and accounted for.
+func TestInlineLibrariesLeavesAKerMLLibraryWithNoSysMLSpelling(t *testing.T) {
+ notation := []byte("part p { ref s : StateActivity::StateAction; }\n")
+ out, s := inlineLibraries(notation)
+ if string(out) != string(notation) {
+ t.Errorf("output changed:\n%s", out)
+ }
+ if len(s.Inlined) > 0 || s.NotInlined["StateActivity"] == "" {
+ t.Errorf("inlined %v, not inlined %v; want StateActivity not inlined", s.Inlined, s.NotInlined)
+ }
+ if summary := (&Report{Libraries: s}).Summary(); !strings.Contains(summary, "StateActivity not inlined: a KerML library with no SysML spelling") {
+ t.Errorf("summary = %s", summary)
+ }
+}
+
+// A name that is a library's only as a nested member does not call for the library.
+func TestInlineLibrariesIgnoresAnUnqualifiedName(t *testing.T) {
+ out, s := inlineLibraries([]byte("package Stochastic;\npart p;\n"))
+ if len(s.Inlined) > 0 || strings.Contains(string(out), "library package") {
+ t.Errorf("inlined %v for a package merely named like a library", s.Inlined)
+ }
+}
diff --git a/internal/translate/migrate/report.go b/internal/translate/migrate/report.go
index c3969206cf..f90e5343e2 100644
--- a/internal/translate/migrate/report.go
+++ b/internal/translate/migrate/report.go
@@ -84,6 +84,9 @@ type Report struct {
// Images counts the attached image files the migration wrote beside the
// notation for its Image blocks.
Images int `json:"images,omitempty"`
+ // Libraries accounts for the OpenSysML library packages a portable
+ // migration appended to the output; nil when the migration was not portable.
+ Libraries *LibrarySummary `json:"libraries,omitempty"`
}
// LayoutSummary accounts for what an MTIP export and the diagrams' own symbol
@@ -103,6 +106,9 @@ type LayoutSummary struct {
DiagramsUnmatched int `json:"diagramsUnmatched"`
// ViewsWithoutLayout counts the written views the export does not cover.
ViewsWithoutLayout int `json:"viewsWithoutLayout"`
+ // GeometryOmitted counts the joined or stream-laid-out views whose
+ // geometry a strict migration leaves unwritten, DiagramLayout being OpenSysML's.
+ GeometryOmitted int `json:"geometryOmitted,omitempty"`
// Placements counts the shown elements the export positions;
// PlacementsWritten those positioned into a view, PlacementsUnexposed
// those the view neither exposes nor draws, PlacementsDangling those
@@ -184,13 +190,27 @@ func (r *Report) Summary() string {
total, c[Mapped], c[Approximated], c[Unmapped], c[Skipped]-unreferenced, unreferenced)
if l := r.Layout; l != nil {
laidOut := l.DiagramsJoined + l.StreamDiagrams
- s += fmt.Sprintf("; laid out %d of %d diagrams from %s: %s elements positioned, %s connectors routed, %s styled, %s notes",
- laidOut, laidOut+l.ViewsWithoutLayout, l.Source,
- commas(l.PlacementsWritten), commas(l.RoutesWritten), commas(l.StylesWritten), commas(l.Notes))
+ if l.GeometryOmitted > 0 {
+ s += fmt.Sprintf("; matched %d of %d diagrams to %s, whose geometry the strict migration omits", laidOut, laidOut+l.ViewsWithoutLayout, l.Source)
+ } else {
+ s += fmt.Sprintf("; laid out %d of %d diagrams from %s: %s elements positioned, %s connectors routed, %s styled, %s notes",
+ laidOut, laidOut+l.ViewsWithoutLayout, l.Source,
+ commas(l.PlacementsWritten), commas(l.RoutesWritten), commas(l.StylesWritten), commas(l.Notes))
+ }
}
if r.Images > 0 {
s += fmt.Sprintf("; wrote %d image file(s)", r.Images)
}
+ if l := r.Libraries; l != nil {
+ if len(l.Inlined) == 0 {
+ s += "; refers to no OpenSysML library, none inlined"
+ } else {
+ s += fmt.Sprintf("; inlined %d OpenSysML library package(s): %s", len(l.Inlined), strings.Join(l.Inlined, ", "))
+ }
+ for _, name := range sortedKeys(l.NotInlined) {
+ s += fmt.Sprintf("; %s not inlined: %s", name, l.NotInlined[name])
+ }
+ }
return s
}
@@ -277,10 +297,17 @@ func (l *LayoutSummary) writeText(b *strings.Builder) {
} else {
b.WriteString("# ")
}
- fmt.Fprintf(b, "%d views laid out from their own symbol stream, %d without layout\n", l.StreamDiagrams, l.ViewsWithoutLayout)
+ streamVerb := "laid out from"
+ if l.GeometryOmitted > 0 {
+ streamVerb = "matched to"
+ }
+ fmt.Fprintf(b, "%d views %s their own symbol stream, %d without layout\n", l.StreamDiagrams, streamVerb, l.ViewsWithoutLayout)
if l.StreamSupplemented > 0 {
fmt.Fprintf(b, "# %d joined views supplemented from their own symbol stream where the record placed or routed nothing\n", l.StreamSupplemented)
}
+ if l.GeometryOmitted > 0 {
+ fmt.Fprintf(b, "# %d views' geometry omitted by the strict migration\n", l.GeometryOmitted)
+ }
fmt.Fprintf(b, "# placements: %d of %d written (%d not exposed, %d resolving to no element); routes: %d of %d written (%d not pinned, %d resolving to no element); malformed: %d\n",
l.PlacementsWritten, l.Placements, l.PlacementsUnexposed, l.PlacementsDangling,
l.RoutesWritten, l.Routes, l.RoutesUnexposed, l.RoutesDangling, l.Malformed)
diff --git a/internal/translate/migrate/results.go b/internal/translate/migrate/results.go
index 38909a0bf9..beaa4460c6 100644
--- a/internal/translate/migrate/results.go
+++ b/internal/translate/migrate/results.go
@@ -206,7 +206,7 @@ func (m *migration) snapshotNotes(r *simresults.ConfigurationResults, scan *snap
}
// sortedKeys is the keys of counts, sorted.
-func sortedKeys(counts map[string]int) []string {
+func sortedKeys[V any](counts map[string]V) []string {
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
diff --git a/internal/translate/migrate/simconfig.go b/internal/translate/migrate/simconfig.go
index 6313e06a82..ccfd922c07 100644
--- a/internal/translate/migrate/simconfig.go
+++ b/internal/translate/migrate/simconfig.go
@@ -144,12 +144,20 @@ func (m *migration) simulationConfig(e *sysmlv1.Element, header, note string) {
results.Notes = append(results.Notes, lost...)
notes = append(notes, lost...)
note = joinNotes(note, strings.Join(notes, "; "))
+ if m.strict {
+ note = joinNotes(note, "its run settings are written as a comment: a strict migration names no OpenSysML library, and Simulation::Configuration is one's")
+ }
m.add(e, verdictFor(note), m.v2Name(e), note)
m.w.block(header, func() {
saved := m.scope
m.scope = e
m.comments(e)
- m.w.block("@Simulation::Configuration", func() { m.w.lines(settings.lines) })
+ if m.strict {
+ m.w.lines(commentLines("simulation run settings, a comment since their v2 form is an OpenSysML library's: " +
+ strings.ReplaceAll(strings.Join(settings.lines, " "), "Simulation::DrawPolicy::", "")))
+ } else {
+ m.w.block("@Simulation::Configuration", func() { m.w.lines(settings.lines) })
+ }
if target.element != nil {
part := m.freshName(e, "target")
results.Target = part
diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go
index b2b06658ac..cc9f824a52 100644
--- a/internal/translate/migrate/states.go
+++ b/internal/translate/migrate/states.go
@@ -33,18 +33,42 @@ func theTransition(t *sysmlv1.Element) string { return "the transition " + descr
func deferredBy(v *sysmlv1.Element) string { return "deferred by " + describe(v) }
-// pseudostateLine spells the member a pseudostate of kind kw writes as: a
-// StateMachines metadata usage for choice/junction/history — qualified so a
-// member named like the metadata cannot shadow it — the standard literal for
-// fork/join.
-func pseudostateLine(prefix, kw, name string) string {
+// pseudostateLine spells the member a pseudostate of kind kw writes as in
+// scope: a StateMachines metadata usage for choice/junction/history — qualified
+// so a member named like the metadata cannot shadow it, importing the library —
+// the standard literal for fork/join. A strict migration, naming no OpenSysML
+// library, writes a choice or junction as a plain state.
+func (m *migration) pseudostateLine(scope *sysmlv1.Element, kw, name string) string {
switch kw {
case "choice", "junction", "shallowHistory", "deepHistory":
- return "#" + prefix + "StateMachines::" + kw + " state " + name + ";"
+ if m.strict {
+ return "state " + name + ";"
+ }
+ m.useStateMachines()
+ return "#" + m.stateMachinesPrefix(scope) + "StateMachines::" + kw + " state " + name + ";"
}
return kw + " " + name + ";"
}
+// junctionSpelling says how a junction is written, for a report note.
+func (m *migration) junctionSpelling() string {
+ if m.strict {
+ return "plain `state` standing for a junction, a strict migration naming no OpenSysML library,"
+ }
+ return "`#StateMachines::junction state`"
+}
+
+// historyWords says what each history pseudostate re-enters, for a report note.
+var historyWords = map[string]string{
+ "shallowHistory": "shallow history, which re-enters the substate active when its state was last left",
+ "deepHistory": "deep history, which re-enters the innermost states active when its state was last left",
+}
+
+// noStrictHistory says why a strict migration refuses a history pseudostate.
+func noStrictHistory(what string) string {
+ return what + " has no standard v2 form: a history pseudostate is written only through the OpenSysML StateMachines library, which a strict migration does not name"
+}
+
// stateMachinesPrefix is "$::" when a member written as `StateMachines` hides
// the library package from the scope a clause is written in, so the metadata
// reference names it from the root.
@@ -59,6 +83,9 @@ func (m *migration) stateMachinesPrefix(scope *sysmlv1.Element) string {
// innermost package enclosing the current scope, once, for the StateMachines
// metadata member being written.
func (m *migration) useStateMachines() {
+ if m.strict {
+ return
+ }
for e := m.scope; e != nil; e = e.Parent {
switch e.Type {
case "Package", "Model", "Profile":
@@ -353,7 +380,7 @@ func (m *migration) entryPointForm(v, owner *sysmlv1.Element) pointForm {
}
regions := m.regionsCrossed(out, owner, "target")
if len(out) < 2 || len(regions) < 2 {
- return pointForm{kw: "junction", note: "written as a `#StateMachines::junction state` of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction"}
+ return pointForm{kw: "junction", note: "written as a " + m.junctionSpelling() + " of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction"}
}
if len(regions) < len(out) {
return pointForm{why: "the entry point starts several regions of " + describe(owner) + ", as a fork does, but two of its outgoing transitions enter the same region"}
@@ -416,7 +443,7 @@ func (m *migration) exitPointForm(v, owner *sysmlv1.Element) pointForm {
}
regions := m.regionsCrossed(in, owner, "source")
if len(in) < 2 || len(regions) < 2 {
- return pointForm{kw: "junction", note: "written as a `#StateMachines::junction state` of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it"}
+ return pointForm{kw: "junction", note: "written as a " + m.junctionSpelling() + " of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it"}
}
joinBut := "several regions of " + describe(owner) + " leave through the exit point, as through a join, but "
if len(regions) < len(in) {
@@ -1118,28 +1145,29 @@ func (s *stateRegion) vertex(v *sysmlv1.Element) {
switch pseudoKind(v) {
case "initial":
case "choice", "junction":
- name := writeName(s.name(v))
- s.m.useStateMachines()
- s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), pseudoKind(v), name))
+ name, kind := writeName(s.name(v)), pseudoKind(v)
+ s.m.w.line(s.m.pseudostateLine(v.Parent, kind, name))
s.m.madeUp(v, name)
- s.m.add(v, Mapped, name, "written as a #StateMachines::"+pseudoKind(v)+" state pseudostate, whose guarded transitions the runtime reads when it is reached; an unguarded one is its else branch")
+ if s.m.strict {
+ s.m.add(v, Approximated, name, "written as a plain state standing for the "+kind+" pseudostate, since a strict migration names no OpenSysML library and #StateMachines::"+kind+" is one's; its guarded transitions are the pseudostate's, but a runtime no longer passes through it in one step")
+ } else {
+ s.m.add(v, Mapped, name, "written as a #StateMachines::"+kind+" state pseudostate, whose guarded transitions the runtime reads when it is reached; an unguarded one is its else branch")
+ }
case "fork", "join":
name := writeName(s.name(v))
s.m.w.line(pseudoKind(v) + " " + name + ";")
s.m.madeUp(v, name)
s.m.add(v, Mapped, name, "written as a "+pseudoKind(v)+" pseudostate, whose segments the runtime fires together")
- case "shallowHistory":
- name := writeName(s.name(v))
- s.m.useStateMachines()
- s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), "shallowHistory", name))
- s.m.madeUp(v, name)
- s.m.add(v, Mapped, name, "written as a `#StateMachines::shallowHistory state` shallow history, which re-enters the substate active when its state was last left")
- case "deepHistory":
+ case "shallowHistory", "deepHistory":
+ kind := pseudoKind(v)
+ if s.m.strict {
+ s.m.unmapped(v, noStrictHistory("the "+kind+" pseudostate"))
+ return
+ }
name := writeName(s.name(v))
- s.m.useStateMachines()
- s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), "deepHistory", name))
+ s.m.w.line(s.m.pseudostateLine(v.Parent, kind, name))
s.m.madeUp(v, name)
- s.m.add(v, Mapped, name, "written as a `#StateMachines::deepHistory state` deep history, which re-enters the innermost states active when its state was last left")
+ s.m.add(v, Mapped, name, "written as a `#StateMachines::"+kind+" state` "+historyWords[kind])
case "terminate":
s.m.add(v, Approximated, "done", "a terminate pseudostate ends the machine; a transition to it is written to done, which ends its region")
case "entryPoint", "exitPoint":
@@ -1188,10 +1216,7 @@ func (m *migration) statePoints(v *sysmlv1.Element) int {
m.add(cp, Mapped, m.vertexNames[v], f.note)
default:
name := writeName(m.vertexNames[cp])
- if f.kw == "junction" {
- m.useStateMachines()
- }
- m.w.line(pseudostateLine(m.stateMachinesPrefix(m.scope), f.kw, name))
+ m.w.line(m.pseudostateLine(m.scope, f.kw, name))
m.madeUp(cp, name)
m.add(cp, Mapped, name, f.note)
written++
@@ -1877,7 +1902,9 @@ func (s *stateRegion) transitionWritten(t *sysmlv1.Element) bool {
return named(pointOwner(tgt))
}
return named(tgt)
- case "choice", "junction", "shallowHistory", "deepHistory", "fork", "join":
+ case "shallowHistory", "deepHistory":
+ return !s.m.strict && named(tgt)
+ case "choice", "junction", "fork", "join":
return named(tgt)
}
case "ConnectionPointReference":
@@ -2391,7 +2418,13 @@ func (s *stateRegion) target(t, v *sysmlv1.Element) (string, bool) {
return p, ok
}
return s.endpoint(t, v, "target")
- case "choice", "junction", "shallowHistory", "deepHistory":
+ case "shallowHistory", "deepHistory":
+ if s.m.strict {
+ s.m.add(t, Unmapped, "", noStrictHistory("its target "+describe(v)))
+ return "", false
+ }
+ return s.endpoint(t, v, "target")
+ case "choice", "junction":
return s.endpoint(t, v, "target")
case "fork", "join":
return s.endpoint(t, v, "target")
@@ -2418,7 +2451,13 @@ func (s *stateRegion) source(t, v *sysmlv1.Element) (string, bool) {
return "", false
}
return s.endpoint(t, v, "source")
- case "choice", "junction", "shallowHistory", "deepHistory":
+ case "shallowHistory", "deepHistory":
+ if s.m.strict {
+ s.m.add(t, Unmapped, "", noStrictHistory("its source "+describe(v)))
+ return "", false
+ }
+ return s.endpoint(t, v, "source")
+ case "choice", "junction":
return s.endpoint(t, v, "source")
case "fork", "join":
return s.endpoint(t, v, "source")
@@ -2970,6 +3009,9 @@ func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) {
return "", ""
}
if k := pseudoKind(src); (k == "choice" || k == "junction") && isElseGuard(spec) {
+ if s.m.strict {
+ return s.strictElse(g, t, src, k)
+ }
s.m.add(g, Mapped, "", "an else guard is written as the unguarded transition out of the "+k+", which is taken when no guarded one holds")
return "", ""
}
@@ -2983,6 +3025,47 @@ func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) {
return " if " + expr, note
}
+// strictElse writes an else guard out of a choice or junction a strict migration
+// writes as a plain state, whose transitions compete rather than yield to a
+// holding guard: the negation of the other written transitions' guards when
+// each is a v2 expression, else unguarded with a note on what that risks.
+func (s *stateRegion) strictElse(g, t, src *sysmlv1.Element, kind string) (string, string) {
+ var others []string
+ for _, o := range s.m.outgoing[src] {
+ if o == t || !s.transitionWritten(o) {
+ continue
+ }
+ og := s.m.guardOf(o)
+ spec := firstOwned(og, "specification")
+ switch {
+ case og == nil, spec == nil, trueLiteral(spec):
+ others = append(others, "true")
+ continue
+ case isElseGuard(spec):
+ continue
+ }
+ expr, ok, _ := s.m.behaviorValue(spec, o)
+ if !ok {
+ note := "the else guard is written as an unguarded transition, since the guard [" + describeValue(spec) + "] of the transition " + describe(o) + " is not a v2 expression to negate; the " + kind + " is a plain state whose transitions compete, so this branch may be taken while that guard holds"
+ s.m.add(g, Approximated, "", note)
+ return "", note
+ }
+ others = append(others, expr)
+ }
+ if len(others) == 0 {
+ s.m.add(g, Mapped, "", "an else guard beside no guarded transition out of the "+kind+" is not written")
+ return "", ""
+ }
+ for i, o := range others {
+ if len(others) > 1 {
+ others[i] = "(" + o + ")"
+ }
+ }
+ note := "the else guard is written as the negation of the other guards out of the " + kind + ", a plain state whose transitions compete rather than fall back to the else branch"
+ s.m.add(g, Approximated, "", note)
+ return " if not (" + strings.Join(others, " or ") + ")", note
+}
+
// isElseGuard reports whether a guard's specification is the word else, which
// a v1 tool writes on the branch a choice takes when no other guard holds.
func isElseGuard(spec *sysmlv1.Element) bool {
diff --git a/internal/translate/migrate/tables.go b/internal/translate/migrate/tables.go
index 9b646d3d65..1335bfc387 100644
--- a/internal/translate/migrate/tables.go
+++ b/internal/translate/migrate/tables.go
@@ -201,6 +201,10 @@ func (m *migration) lowerTable(td *tableDoc) {
t, host := td.t, td.v.host
l := &lowered{}
td.l = l
+ if m.strict {
+ l.refuse("its v2 form is a document of OpenSysML's DocumentQueries library, which a strict migration does not name")
+ return
+ }
if len(t.Malformed) > 0 {
l.refuse(strings.Join(t.Malformed, "; "))
}
diff --git a/internal/translate/migrate/translate.go b/internal/translate/migrate/translate.go
index e6db49bece..2c5a61f220 100644
--- a/internal/translate/migrate/translate.go
+++ b/internal/translate/migrate/translate.go
@@ -89,6 +89,8 @@ func (m *migration) lanesAround(e *sysmlv1.Element) (*lanes, *sysmlv1.Element) {
// first, then what the scope sees, each further step a feature of the last.
// The result is plural once any object the name reads through is, and a
// write through a collection is refused: it would reach several objects.
+func (s *bodyScope) strictMigration() bool { return s.m.strict }
+
func (s *bodyScope) feature(path []string, write bool) (opaqueRef, *refusal) {
m := s.m
full := strings.Join(path, ".")
@@ -325,6 +327,8 @@ type nameReads struct {
names []string
}
+func (r *nameReads) strictMigration() bool { return r.s.strictMigration() }
+
func (r *nameReads) feature(path []string, write bool) (opaqueRef, *refusal) {
if r.s.m.pinCalled(r.s.scope, path[0]) != nil {
return opaqueRef{expr: strings.Join(path, ".")}, nil
diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1
index 53fb8c3625..3166bc9d2e 100644
--- a/packaging/man/man1/sysml.1
+++ b/packaging/man/man1/sysml.1
@@ -39,7 +39,8 @@ Report the model's diagnostics and exit, nonzero on an error;
.TP
.B \-strict
Judge the model as conforming SysML v2: notation no pinned production admits
-is an error, not a warning; a SysML v1 migration writes none of it
+is an error, not a warning; a SysML v1 migration writes none of it and refers
+to no OpenSysML library
.TP
.BR \-disable\-lint " \fIcode\fP"
Leave this lint out of the model's diagnostics: undeclared\-signal,
@@ -281,6 +282,11 @@ geometry it records as DiagramLayout annotations in the migrated views
With \-migrate, the http(s) URL a comment's relative
is resolved
against, such as the View Editor server
.TP
+.B \-portable
+With \-migrate, append the OpenSysML library packages the output refers to, so
+the one file loads in a tool that ships only the standard library; the report
+names them
+.TP
.BR \-compare\-results " \fIfile\fP"
Run every configuration this \-migration\-results file indexes \(em or those
\-action names \(em with its recorded runs and duration mode, or the \-runs
diff --git a/tests/migrate/behavior_more_test.go b/tests/migrate/behavior_more_test.go
index b28907cbdd..581a9828a6 100644
--- a/tests/migrate/behavior_more_test.go
+++ b/tests/migrate/behavior_more_test.go
@@ -174,11 +174,14 @@ func TestStateMachineWithOrthogonalRegionsAndGuards(t *testing.T) {
wantNote(t, r, "_dNoon", migrate.Unmapped, "only a signal event can be deferred, not a TimeEvent")
// Strict output carries the deferral in the same standard notation; the
- // choice pseudostate is written as metadata in both modes.
+ // choice pseudostate is written as a plain state, StateMachines being
+ // OpenSysML's library.
strict := migrateDocumentOptions(t, ovenMachine, ovenApplications, migrate.Options{Strict: true})
wantDeferredDoorEncoding(t, strict)
wantNote(t, strict, "_dNoon", migrate.Unmapped, "only a signal event can be deferred, not a TimeEvent")
- wantNote(t, strict, "_pick", migrate.Mapped, "written as a #StateMachines::choice state pseudostate")
+ wantNote(t, strict, "_pick", migrate.Approximated, "written as a plain state standing for the choice pseudostate")
+ wantLine(t, strict.Notation, "state choice;")
+ wantNoLine(t, strict.Notation, "StateMachines::choice")
wantNoLine(t, strict.Notation, "choice choice;")
noExtensionStatement(t, strict.Notation)
wantNote(t, strict, "_gWorn", migrate.Mapped, "")
@@ -259,14 +262,14 @@ func wantDeferredDoorEncoding(t *testing.T, r *migrate.Result) {
"#MigrationMetadata::DeferredKeeper action receive accept kept : Door;",
"then action keep { assign deferred := SequenceFunctions::including(deferred, receive.kept); }",
"then receive;",
- "metadata MigrationMetadata::SynthesizedName about receive, keep;",
"exit action flush {",
"for kept in deferred { send kept to self; }",
"then action clear { assign deferred := (); }",
- "metadata MigrationMetadata::SynthesizedName about deferred, buffer, flush;",
} {
wantLine(t, r.Notation, line)
}
+ wantMadeUp(t, r.Notation, "receive, keep")
+ wantMadeUp(t, r.Notation, "deferred, buffer, flush")
wantNote(t, r, "_dDoor", migrate.Approximated, "kept in the item deferred by the accept loop of the do action buffer while the state is active, and sent to self by the exit action flush: the standard SysML v2 encoding of a deferred signal, which the state's @MigrationMetadata::DeferredEvent annotation records")
wantNote(t, r, "_doorEv", migrate.Approximated, "deferred by 'Off' through the standard SysML v2 encoding, an accept loop keeping the signal while the state is active and an exit action sending it to self")
}
@@ -363,8 +366,8 @@ func TestStrictDeferredSignalsAreKeptAndReplayed(t *testing.T) {
checkDeferredSignalsAreKeptAndReplayed(t, migrateDocumentOptions(t, deferringMachine, deferringApplications, mode.opts))
})
}
- if d, s := migrateDocument(t, deferringMachine, deferringApplications), migrateDocumentOptions(t, deferringMachine, deferringApplications, migrate.Options{Strict: true}); string(d.Notation) != string(s.Notation) {
- t.Errorf("default and strict migrations of the deferring machine differ:\n--- default\n%s\n--- strict\n%s", d.Notation, s.Notation)
+ if d, s := migrateDocument(t, deferringMachine, deferringApplications), migrateDocumentOptions(t, deferringMachine, deferringApplications, migrate.Options{Strict: true}); withoutNameMarkers(d.Notation) != withoutNameMarkers(s.Notation) {
+ t.Errorf("default and strict migrations of the deferring machine differ past the made-up name markers:\n--- default\n%s\n--- strict\n%s", d.Notation, s.Notation)
}
}
@@ -397,7 +400,6 @@ func checkDeferredSignalsAreKeptAndReplayed(t *testing.T, r *migrate.Result) {
"#MigrationMetadata::DeferredKeeper action receiveBeep accept keptBeep : Beep;",
"then action keepBeep { assign deferredBeep := SequenceFunctions::including(deferredBeep, receiveBeep.keptBeep); }",
"then receiveBeep;",
- "metadata MigrationMetadata::SynthesizedName about split, run, receiveAlarm, keepAlarm, receiveBeep, keepBeep;",
"exit action flush {",
"action leave {",
"assign context.exits := context.exits + 1;",
@@ -405,14 +407,15 @@ func checkDeferredSignalsAreKeptAndReplayed(t *testing.T, r *migrate.Result) {
"then action clearAlarm { assign deferredAlarm := (); }",
"then for keptBeep in deferredBeep { send keptBeep to self; }",
"then action clearBeep { assign deferredBeep := (); }",
- "metadata MigrationMetadata::SynthesizedName about clearAlarm, clearBeep;",
- "metadata MigrationMetadata::SynthesizedName about deferredAlarm, deferredBeep, buffer, flush;",
"transition first Waiting accept Go then Working;",
"transition first Busy accept Alarm then Alarmed;",
"transition first Working accept Beep then Done;",
} {
wantLine(t, r.Notation, line)
}
+ wantMadeUp(t, r.Notation, "split, run, receiveAlarm, keepAlarm, receiveBeep, keepBeep")
+ wantMadeUp(t, r.Notation, "clearAlarm, clearBeep")
+ wantMadeUp(t, r.Notation, "deferredAlarm, deferredBeep, buffer, flush")
wantNote(t, r, "_dGo", migrate.Approximated, "the transition (_tGo) out of the state accepts the signal, which in v1 takes precedence over deferring it, so the state does not keep it; its @MigrationMetadata::DeferredEvent annotation records the deferral")
wantNoLine(t, r.Notation, "item deferredGo : Go[*] ordered;")
wantNote(t, r, "_goDeferredEv", migrate.Approximated, "deferred by 'Waiting', which the state's @MigrationMetadata::DeferredEvent annotation records; the state does not keep the signal, the transition (_tGo) accepting it")
@@ -1047,7 +1050,7 @@ func TestDeferralYieldsToChoiceTransitionOnlyWhereWritten(t *testing.T) {
wantNoLine(t, strict.Notation, "choice pick;")
wantNoLine(t, strict.Notation, "item deferred : Door[*] ordered;")
for _, line := range []string{
- "#StateMachines::choice state pick;",
+ "state pick;",
"transition first Off accept Door then pick;",
"transition first pick then Idle;",
"@MigrationMetadata::DeferredEvent { ref :>> signal : Door; }",
@@ -2437,3 +2440,26 @@ func TestDeferralCountsTransitionsToUnnamedTargets(t *testing.T) {
t.Errorf("Idle did not complete into the submachine state:\n%s", out)
}
}
+
+// withoutNameMarkers is the notation less the markers of made-up names, which
+// a default migration writes as MigrationMetadata and a strict one as comments.
+func withoutNameMarkers(notation []byte) string {
+ var kept []string
+ for _, line := range strings.Split(string(notation), "\n") {
+ if strings.Contains(line, "SynthesizedName about") || strings.Contains(line, "// names the migration made up") {
+ continue
+ }
+ kept = append(kept, line)
+ }
+ return strings.Join(kept, "\n")
+}
+
+// wantMadeUp checks the marker of the made-up names, which a default migration
+// writes as MigrationMetadata and a strict one as a comment.
+func wantMadeUp(t *testing.T, notation []byte, names string) {
+ t.Helper()
+ got := string(notation)
+ if !strings.Contains(got, "metadata MigrationMetadata::SynthesizedName about "+names+";") && !strings.Contains(got, "// names the migration made up: "+names) {
+ t.Errorf("notation marks no made-up names %q:\n%s", names, got)
+ }
+}
diff --git a/tests/migrate/behavior_test.go b/tests/migrate/behavior_test.go
index 750aa1b1cf..c86791fede 100644
--- a/tests/migrate/behavior_test.go
+++ b/tests/migrate/behavior_test.go
@@ -458,8 +458,8 @@ func TestPropertyBackedProbabilitiesAreReferences(t *testing.T) {
func TestStrictPropertyProbabilityUsesOnlyLiteralDefaults(t *testing.T) {
r := migrateFixtureFileOptions(t, "decision_property_probability", migrate.Options{Strict: true})
for _, line := range []string{
- "first 'with default' then defaulted { @Stochastic::Probability { p = 0.25; } }",
- "first 'with default' then remainder { @Stochastic::Probability { p = 0.75; } }",
+ "first 'with default' then defaulted; // probability 0.25",
+ "first 'with default' then remainder; // probability 0.75",
} {
wantLine(t, r.Notation, line)
}
@@ -1018,3 +1018,10 @@ func TestNestedDefProbabilityReadsThroughContext(t *testing.T) {
run(t, r)
})
}
+
+func TestStrictProbabilityNoteReachesAnEdgeWhoseGuardIsNotMigrated(t *testing.T) {
+ r := migrateDocumentOptions(t, missionActivity, missionApplications, migrate.Options{Strict: true})
+ wantNote(t, r, "_e8", migrate.Approximated, "its probability 0.25 is written as a comment")
+ wantNote(t, r, "_e9", migrate.Approximated, "its probability 0.75 is written as a comment")
+ wantNoLine(t, r.Notation, "Stochastic::Probability")
+}
diff --git a/tests/migrate/layout_test.go b/tests/migrate/layout_test.go
index d5c97ab2d4..40d69f56c3 100644
--- a/tests/migrate/layout_test.go
+++ b/tests/migrate/layout_test.go
@@ -115,6 +115,24 @@ func TestGoldenLayout(t *testing.T) {
for _, d := range errorsMode(t, "layout.sysml", strict.Notation, diag.ConformanceStrict) {
t.Errorf("%v", d)
}
+ // The strict migration joins the same records to the same views; only
+ // their geometry is left unwritten, which the summary says instead of
+ // reporting the views as missing.
+ l, sl := r.Report.Layout, strict.Report.Layout
+ if sl.DiagramsJoined != l.DiagramsJoined || sl.DiagramsUnmatched != l.DiagramsUnmatched || sl.ViewsWithoutLayout != l.ViewsWithoutLayout || sl.GeometryOmitted != l.DiagramsJoined || sl.PlacementsWritten != 0 {
+ t.Errorf("strict layout summary: %+v\nwant joined %d, unmatched %d, without %d, all geometry omitted", sl, l.DiagramsJoined, l.DiagramsUnmatched, l.ViewsWithoutLayout)
+ }
+ if l.GeometryOmitted != 0 {
+ t.Errorf("default layout summary omits geometry: %+v", l)
+ }
+ for _, e := range strict.Report.Entries {
+ if e.Kind == "Layout" && strings.Contains(e.Note, "does not write as a view") {
+ t.Errorf("strict migration reports a joined view missing: %+v", e)
+ }
+ }
+ if !strings.Contains(strict.Report.Summary(), "geometry the strict migration omits") {
+ t.Errorf("strict summary = %q", strict.Report.Summary())
+ }
}
func TestGoldenLayoutExposedMetadataAttribute(t *testing.T) {
diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go
index 94d039855e..ef2a65a406 100644
--- a/tests/migrate/migrate_test.go
+++ b/tests/migrate/migrate_test.go
@@ -438,6 +438,21 @@ func TestGoldenConstructFixtures(t *testing.T) {
// A strict migration writes only notation a pinned SysML v2 production admits:
// every fixture's strict output analyses with zero error diagnostics in strict
// conformance mode, where extension notation is an error.
+// A strict migration refuses a DocGen document before planning its content,
+// so the report holds an Unmapped row for the document and each view and
+// paragraph in it, and no Mapped row for content no Document holds.
+func TestStrictMigrationRefusesDocumentsBeforePlanningThem(t *testing.T) {
+ r := migrateFixtureFileOptions(t, "montecarlo_docgen_scoped", migrate.Options{Strict: true})
+ wantLine(t, r.Notation, "/* not migrated: «Document» 'Statistics Report' — its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name */")
+ if strings.Contains(string(r.Notation), "DocumentQueries::") {
+ t.Errorf("strict notation names DocumentQueries:\n%s", r.Notation)
+ }
+ wantNote(t, r, "_s8", migrate.Unmapped, "its v2 form is a Document of OpenSysML's DocumentQueries library")
+ for _, id := range []string{"_s4", "_s5"} {
+ wantNote(t, r, id, migrate.Unmapped, "it belongs to «Document» 'Statistics Report', which is not migrated")
+ }
+}
+
func TestStrictMigrationAnalysesCleanUnderStrictConformance(t *testing.T) {
for _, name := range append([]string{"vehicle"}, constructFixtures...) {
t.Run(name, func(t *testing.T) {
diff --git a/tests/migrate/portable_test.go b/tests/migrate/portable_test.go
new file mode 100644
index 0000000000..39fd6acdd1
--- /dev/null
+++ b/tests/migrate/portable_test.go
@@ -0,0 +1,83 @@
+package migrate_test
+
+import (
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/Open-MBEE/OpenSysML/internal/translate/migrate"
+)
+
+var standardDeclaration = regexp.MustCompile(`(?m)^standard library package`)
+
+const inlinedLibrariesMark = "\n// OpenSysML library packages the model refers to"
+
+// A portable migration appends every OpenSysML library package its notation
+// refers to, as a library package a user file may declare, and accounts for
+// them; the default migration appends none and accounts for none.
+func TestPortableMigrationInlinesTheLibrariesItRefersTo(t *testing.T) {
+ fixtures, err := filepath.Glob(filepath.Join("testdata", "xmi", "*.xmi"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(fixtures) == 0 {
+ t.Fatal("no fixture found")
+ }
+ libs := openSysMLLibraries(t)
+ for _, f := range fixtures {
+ name := strings.TrimSuffix(filepath.Base(f), ".xmi")
+ t.Run(name, func(t *testing.T) {
+ r := migrateFixtureFileOptions(t, name, migrate.Options{Portable: true})
+ model, appended, _ := strings.Cut(string(r.Notation), inlinedLibrariesMark)
+ if r.Report.Libraries == nil {
+ t.Fatal("report accounts for no libraries")
+ }
+ got := r.Report.Libraries.Inlined
+ var want []string
+ for _, lib := range libs {
+ if strings.Contains(model, lib+"::") {
+ want = append(want, lib)
+ }
+ }
+ for _, lib := range want {
+ if !slices.Contains(got, lib) {
+ t.Errorf("%s referred to but not accounted for in %v", lib, got)
+ }
+ if !strings.Contains(appended, "library package "+lib+" {") {
+ t.Errorf("%s referred to but not inlined", lib)
+ }
+ }
+ if len(want) == 0 && (appended != "" || len(got) > 0) {
+ t.Errorf("nothing referred to, yet inlined %v", got)
+ }
+ if standardDeclaration.MatchString(appended) {
+ t.Error("an inlined package is marked standard")
+ }
+ if summary := r.Report.Summary(); len(want) > 0 && !strings.Contains(summary, "inlined "+itoa(len(got))+" OpenSysML library package(s): ") {
+ t.Errorf("summary does not account for the libraries: %s", summary)
+ }
+ wantClean(t, name+".sysml", r)
+ })
+ }
+ if r := migrateFixtureFile(t, "vehicle"); r.Report.Libraries != nil || strings.Contains(string(r.Notation), inlinedLibrariesMark) {
+ t.Error("a default migration inlined libraries")
+ }
+}
+
+func itoa(n int) string { return strconv.Itoa(n) }
+
+// A KerML library is inlined in its SysML spelling, its functions as calc defs.
+func TestPortableMigrationSpellsAKerMLLibraryInSysML(t *testing.T) {
+ r := migrateDocumentOptions(t, missionActivity, missionApplications, migrate.Options{Portable: true})
+ wantLine(t, r.Notation, "action wait2 accept after RandomFunctions::uniform(1.0, 8.0) [SI::s];")
+ wantLine(t, r.Notation, "library package RandomFunctions {")
+ wantLine(t, r.Notation, "calc def uniform {")
+ wantNoLine(t, r.Notation, "function uniform {")
+ if got := r.Report.Libraries.Inlined; !slices.Contains(got, "RandomFunctions") {
+ t.Errorf("inlined %v, want RandomFunctions among them", got)
+ }
+ wantClean(t, "t.sysml", r)
+}
diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go
index 00269b9041..70b351da45 100644
--- a/tests/migrate/states_test.go
+++ b/tests/migrate/states_test.go
@@ -2,6 +2,8 @@ package migrate_test
import (
"fmt"
+ "os"
+ "path/filepath"
"strings"
"testing"
@@ -1627,50 +1629,52 @@ func noExtensionStatement(t *testing.T, notation []byte) {
func TestStrictMigrationWritesNoExtensionNotation(t *testing.T) {
r := migrateFixtureFileOptions(t, "plant_states", migrate.Options{Strict: true})
noExtensionStatement(t, r.Notation)
+ noOpenSysMLLibrary(t, r.Notation)
for _, line := range []string{
- "private import StateMachines::*;",
- "#StateMachines::junction state route;",
- "#StateMachines::shallowHistory state last;",
- "#StateMachines::deepHistory state deepest;",
+ "state route;",
"fork spread;",
"join gather;",
+ "transition first route if context.count < 2 then Work;",
+ "transition first route if not (context.count < 2) then done;",
} {
wantLine(t, r.Notation, line)
}
- wantNote(t, r, "_junc", migrate.Mapped, "written as a #StateMachines::junction state pseudostate")
- wantNote(t, r, "_hist", migrate.Mapped, "written as a `#StateMachines::shallowHistory state` shallow history")
- wantNote(t, r, "_deep", migrate.Mapped, "written as a `#StateMachines::deepHistory state` deep history")
- for _, id := range []string{"_tRoute", "_tBusy", "_tSpent", "_tResume", "_tHist"} {
+ wantNote(t, r, "_junc", migrate.Approximated, "written as a plain state standing for the junction pseudostate")
+ wantNote(t, r, "_hist", migrate.Unmapped, "the shallowHistory pseudostate has no standard v2 form")
+ wantNote(t, r, "_deep", migrate.Unmapped, "the deepHistory pseudostate has no standard v2 form")
+ for _, id := range []string{"_tRoute", "_tBusy"} {
wantNote(t, r, id, migrate.Mapped, "")
}
- wantNote(t, r, "_gSpent", migrate.Mapped, "an else guard is written as the unguarded transition out of the junction")
- wantNote(t, r, "_resumeEv", migrate.Mapped, "written where a trigger refers to it, as accept Resume")
+ wantNote(t, r, "_tResume", migrate.Unmapped, "its target 'last' has no standard v2 form")
+ wantNote(t, r, "_tHist", migrate.Unmapped, "its source 'last' has no standard v2 form")
+ wantNote(t, r, "_tSpent", migrate.Approximated, "the else guard is written as the negation of the other guards out of the junction")
+ wantNote(t, r, "_gSpent", migrate.Approximated, "the else guard is written as the negation of the other guards out of the junction, a plain state whose transitions compete rather than fall back to the else branch")
wantNote(t, r, "_fork", migrate.Mapped, "written as a fork pseudostate")
wantNote(t, r, "_join", migrate.Mapped, "written as a join pseudostate")
r = migrateFixtureFileOptions(t, "station_points", migrate.Options{Strict: true})
noExtensionStatement(t, r.Notation)
+ noOpenSysMLLibrary(t, r.Notation)
for _, line := range []string{
- "private import StateMachines::*;",
- "#StateMachines::junction state Start;",
- "#StateMachines::junction state Leave;",
- "#StateMachines::junction state Deep;",
- "#StateMachines::junction state Out;",
- "#StateMachines::shallowHistory state H;",
+ "state Start;",
+ "state Leave;",
+ "state Deep;",
+ "state Out;",
} {
wantLine(t, r.Notation, line)
}
- wantNote(t, r, "_start", migrate.Mapped, "written as a `#StateMachines::junction state` of its state; a transition entering through it runs the state's entry behavior")
- wantNote(t, r, "_leave", migrate.Mapped, "written as a `#StateMachines::junction state` of its state; a transition leaving through it")
- wantNote(t, r, "_deep", migrate.Mapped, "written as a `#StateMachines::junction state` of its state")
- wantNote(t, r, "_out", migrate.Mapped, "written as a `#StateMachines::junction state` of its state")
- wantNote(t, r, "_hist", migrate.Mapped, "written as a `#StateMachines::shallowHistory state` shallow history")
+ wantNote(t, r, "_start", migrate.Mapped, "written as a plain `state` standing for a junction, a strict migration naming no OpenSysML library, of its state; a transition entering through it runs the state's entry behavior")
+ wantNote(t, r, "_leave", migrate.Mapped, "written as a plain `state` standing for a junction, a strict migration naming no OpenSysML library, of its state; a transition leaving through it")
+ wantNote(t, r, "_deep", migrate.Mapped, "written as a plain `state` standing for a junction")
+ wantNote(t, r, "_out", migrate.Mapped, "written as a plain `state` standing for a junction")
+ wantNote(t, r, "_hist", migrate.Unmapped, "the shallowHistory pseudostate has no standard v2 form")
wantNote(t, r, "_both", migrate.Mapped, "written as a fork of its state")
wantNote(t, r, "_gather", migrate.Mapped, "written as a join of its state")
wantNote(t, r, "_plain", migrate.Mapped, "no transition leaves the entry point")
- for _, id := range []string{"_tGo", "_tDive", "_tDeep", "_tBack", "_tFinish", "_tLeave", "_tOut", "_tResume", "_tStart"} {
+ for _, id := range []string{"_tGo", "_tDive", "_tDeep", "_tBack", "_tFinish", "_tLeave", "_tOut", "_tStart"} {
wantNote(t, r, id, migrate.Mapped, "")
}
+ wantNote(t, r, "_tResume", migrate.Unmapped, "its target 'H' has no standard v2 form")
// The pass-through route through 'Through' is still refused: it is not the
// metadata spelling's doing but an unwritable route shape.
wantNote(t, r, "_through", migrate.Unmapped, "leads from the entry point straight to the exit point")
@@ -1724,3 +1728,160 @@ func TestStateMachinesPackageShadowsTheLibrary(t *testing.T) {
wantLine(t, r.Notation, "private import $::StateMachines::*;")
wantLine(t, r.Notation, "#$::StateMachines::choice state pick;")
}
+
+// openSysMLLibraries names the library packages OpenSysML ships beyond the
+// standard's, which a strict migration must not refer to.
+func openSysMLLibraries(t *testing.T) []string {
+ t.Helper()
+ entries, err := os.ReadDir(filepath.Join("..", "..", "internal", "workspace", "libs", "stdlib", "OpenSysML Libraries"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var names []string
+ for _, e := range entries {
+ if name := strings.TrimSuffix(strings.TrimSuffix(e.Name(), ".sysml"), ".kerml"); name != e.Name() {
+ names = append(names, name)
+ }
+ }
+ if len(names) == 0 {
+ t.Fatal("no OpenSysML library found")
+ }
+ return names
+}
+
+// noOpenSysMLLibrary fails on a line of notation referring to an OpenSysML
+// library, past the deferral markers a strict migration keeps: they annotate
+// standard notation the runtime reads them to schedule.
+func noOpenSysMLLibrary(t *testing.T, notation []byte) {
+ t.Helper()
+ libs := openSysMLLibraries(t)
+ for _, line := range strings.Split(string(notation), "\n") {
+ if strings.Contains(line, "MigrationMetadata::DeferredEvent") || strings.Contains(line, "MigrationMetadata::DeferredKeeper") {
+ continue
+ }
+ for _, lib := range libs {
+ if strings.Contains(line, lib+"::") || strings.Contains(line, "import "+lib+";") {
+ t.Errorf("strict notation refers to the OpenSysML library %s:\n%s", lib, line)
+ }
+ }
+ }
+}
+
+// TestStrictMigrationRefersToNoOpenSysMLLibrary migrates every fixture under
+// strict and checks the notation names only the standard library, so a tool
+// without OpenSysML's libraries loads it.
+func TestStrictMigrationRefersToNoOpenSysMLLibrary(t *testing.T) {
+ fixtures, err := filepath.Glob(filepath.Join("testdata", "xmi", "*.xmi"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(fixtures) == 0 {
+ t.Fatal("no fixture found")
+ }
+ for _, f := range fixtures {
+ name := strings.TrimSuffix(filepath.Base(f), ".xmi")
+ t.Run(name, func(t *testing.T) {
+ r := migrateFixtureFileOptions(t, name, migrate.Options{Strict: true})
+ noOpenSysMLLibrary(t, r.Notation)
+ })
+ }
+}
+
+// A choice whose other guard is not a v2 expression leaves a strict
+// migration nothing to negate: the else branch is written unguarded and the
+// report says it may be taken while that guard holds.
+const opaqueChoiceMachine = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Focus lost
+
+
+
+
+ else
+
+
+
+
+ `
+
+// historyChoiceMachine is a choice whose guarded branch targets a shallow
+// history pseudostate, which a strict migration refuses, beside an else branch.
+const historyChoiceMachine = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ n < 2
+
+
+
+
+ else
+
+
+
+
+ `
+
+// A strict else guard negates only the guards of branches the strict migration
+// writes: one into a history pseudostate is refused, so it does not block the fallback.
+func TestStrictChoiceElseIgnoresRefusedBranches(t *testing.T) {
+ r := migrateDocumentOptions(t, historyChoiceMachine, "", migrate.Options{Strict: true})
+ noOpenSysMLLibrary(t, r.Notation)
+ wantLine(t, r.Notation, "state pick;")
+ wantLine(t, r.Notation, "transition first pick then B;")
+ wantNoLine(t, r.Notation, "if not")
+ if es := entriesFor(r, "_htA"); len(es) != 1 || es[0].Verdict != migrate.Unmapped {
+ t.Errorf("_htA entries = %+v, want unmapped", es)
+ }
+ wantNote(t, r, "_hgB", migrate.Mapped, "an else guard beside no guarded transition out of the choice is not written")
+
+ r = migrateDocument(t, historyChoiceMachine, "")
+ wantLine(t, r.Notation, "#StateMachines::shallowHistory state back;")
+ wantNote(t, r, "_hgB", migrate.Mapped, "an else guard is written as the unguarded transition out of the choice")
+}
+
+func TestStrictChoiceElseBesideAnUnmigratedGuardIsReported(t *testing.T) {
+ r := migrateDocumentOptions(t, opaqueChoiceMachine, "", migrate.Options{Strict: true})
+ noOpenSysMLLibrary(t, r.Notation)
+ for _, line := range []string{
+ "state pick;",
+ "/* guard not migrated: [Focus lost] — not v2 expression syntax */",
+ "transition first pick then A;",
+ "transition first pick then B;",
+ } {
+ wantLine(t, r.Notation, line)
+ }
+ wantNote(t, r, "_ogB", migrate.Approximated, "the else guard is written as an unguarded transition, since the guard [Focus lost] of the transition (_otA) is not a v2 expression to negate; the choice is a plain state whose transitions compete, so this branch may be taken while that guard holds")
+
+ r = migrateDocument(t, opaqueChoiceMachine, "")
+ wantNote(t, r, "_ogB", migrate.Mapped, "an else guard is written as the unguarded transition out of the choice")
+}
diff --git a/tests/migrate/streams_test.go b/tests/migrate/streams_test.go
index ef2c9c4a22..9480777186 100644
--- a/tests/migrate/streams_test.go
+++ b/tests/migrate/streams_test.go
@@ -362,6 +362,30 @@ func TestExportPrecedesStream(t *testing.T) {
"_diag_modes", "laid out from modes.layout.xml supplemented by the diagram's own symbol stream: 2 of 3 shown elements positioned")
}
+// A strict migration joins the export's record to its view but writes no
+// geometry, so the report counts the omission, not a supplementation.
+func TestStrictExportIsJoinedButNotSupplemented(t *testing.T) {
+ layout, err := mtip.Parse([]byte(modesExport))
+ if err != nil {
+ t.Fatalf("mtip.Parse: %v", err)
+ }
+ r, err := migrate.MigrateOptions("figures.mdzip", mdzip(t, figureStreams),
+ migrate.Options{Layout: layout, LayoutSource: "modes.layout.xml", Strict: true})
+ if err != nil {
+ t.Fatalf("MigrateOptions: %v", err)
+ }
+ wantNoLine(t, r.Notation, "DiagramLayout")
+ s := r.Report.Layout
+ if s == nil || s.DiagramsJoined != 1 || s.StreamDiagrams != 6 || s.StreamSupplemented != 0 || s.GeometryOmitted != 7 {
+ t.Fatalf("layout summary = %+v; want 1 joined, 6 from streams, none supplemented, 7 omitted", s)
+ }
+ report := reportText(t, r)
+ if strings.Contains(report, "supplemented") {
+ t.Errorf("the strict report claims a supplementation it did not write:\n%s", report)
+ }
+ wantInOrder(t, "omission", report, "_diag_modes", "layout from modes.layout.xml omitted")
+}
+
// An MTIP export placing and routing everything the Pump Modes stream draws
// leaves the stream nothing to supplement, yet its frame still sizes the canvas.
func TestFrameSurvivesCompleteExport(t *testing.T) {