@systemfsoftware/stryker-js-cli 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/main.mjs +254 -103
- package/package.json +15 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @systemfsoftware/stryker-js-cli
|
|
2
2
|
|
|
3
|
+
## 3.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A `--survivors` run given a prior report that exists but does not parse now stops and names the parse failure. It previously reported that no prior report was found — the same message as an absent report — so a corrupted or truncated report looked like a missing one.
|
|
8
|
+
|
|
9
|
+
The exit code is unchanged, and so is every admission verdict: a report whose recorded options, framework version or source content disagree with the current run still reports a mismatch, a report a `--survivors` run produced itself is still refused, an absent report still reports that none was found, and a report with no surviving mutants still ends the run without re-testing.
|
|
10
|
+
|
|
11
|
+
- Fix the CLI refusing to start with a missing-flag error when optional boolean flags were omitted. Starting a mutation run, printing help, or emitting the agent manifest now works without explicitly passing `incremental`, `force`, `ignoreStatic`, or the other boolean flags.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies:
|
|
14
|
+
- @systemfsoftware/effect-cell-types@4.0.0
|
|
15
|
+
- @systemfsoftware/stryker-js-mutation-run@4.0.0
|
|
16
|
+
|
|
3
17
|
## 3.0.0
|
|
4
18
|
|
|
5
19
|
### Major Changes
|
package/dist/main.mjs
CHANGED
|
@@ -34,9 +34,9 @@ import { strykerPlugins } from "@systemfsoftware/stryker-js-mutation-report/stry
|
|
|
34
34
|
import { createHash } from "node:crypto";
|
|
35
35
|
import { readFileSync } from "node:fs";
|
|
36
36
|
import { resolve } from "node:path";
|
|
37
|
-
import { noopLogger } from "@stryker-mutator/util";
|
|
38
37
|
import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
|
|
39
38
|
import "@systemfsoftware/stryker-js-plugin-api/core";
|
|
39
|
+
import { noopLogger } from "@systemfsoftware/stryker-js-util";
|
|
40
40
|
import * as Exit from "effect/Exit";
|
|
41
41
|
import { pipe } from "effect/Function";
|
|
42
42
|
import * as Match from "effect/Match";
|
|
@@ -670,10 +670,6 @@ function sortKeys(value) {
|
|
|
670
670
|
if (isRecord(value)) return objectFromEntries(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
671
671
|
return value;
|
|
672
672
|
}
|
|
673
|
-
/** The single structural hash the admission compares (KTD6). */
|
|
674
|
-
function structuralHash(input, hash) {
|
|
675
|
-
return hash(serializeSurvivorsHashInput(input));
|
|
676
|
-
}
|
|
677
673
|
/**
|
|
678
674
|
* Converts a report mutant (1-based schema location) into the internal mutant
|
|
679
675
|
* shape a run consumes (0-based positions, absolute file name) — the exact
|
|
@@ -729,6 +725,106 @@ function survivorMutateSpans(survivors) {
|
|
|
729
725
|
}
|
|
730
726
|
return spans;
|
|
731
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* The mutant shape the admission carries, named once because both the decision's
|
|
730
|
+
* `Admitted` payload and the command's precomputed survivor list are the same shape.
|
|
731
|
+
*/
|
|
732
|
+
const MutantShape = S.Struct({
|
|
733
|
+
id: S.String,
|
|
734
|
+
fileName: S.String,
|
|
735
|
+
mutatorName: S.String,
|
|
736
|
+
replacement: S.String,
|
|
737
|
+
location: S.Struct({
|
|
738
|
+
start: S.Struct({
|
|
739
|
+
line: S.Finite,
|
|
740
|
+
column: S.Finite
|
|
741
|
+
}),
|
|
742
|
+
end: S.Struct({
|
|
743
|
+
line: S.Finite,
|
|
744
|
+
column: S.Finite
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
});
|
|
748
|
+
/**
|
|
749
|
+
* The prior report as a document, decoded at the boundary. Module-internal: consumers
|
|
750
|
+
* get {@link decodePriorReport}, not the schema, so the report's wire shape is not a
|
|
751
|
+
* surface commitment and the codec has exactly one caller.
|
|
752
|
+
*
|
|
753
|
+
* `status` is a bare string rather than the closed status set on purpose: the decide only
|
|
754
|
+
* compares it to `'Survived'`, so a report written by a newer engine that added a status
|
|
755
|
+
* must not be refused for carrying one.
|
|
756
|
+
*/
|
|
757
|
+
const PriorReportDocument = S.Struct({
|
|
758
|
+
config: S.optional(S.Record(S.String, S.Unknown)),
|
|
759
|
+
framework: S.optional(S.Struct({ version: S.optional(S.String) })),
|
|
760
|
+
files: S.Record(S.String, S.Struct({
|
|
761
|
+
source: S.String,
|
|
762
|
+
mutants: S.Array(S.Struct({
|
|
763
|
+
id: S.String,
|
|
764
|
+
mutatorName: S.String,
|
|
765
|
+
replacement: S.optional(S.String),
|
|
766
|
+
status: S.String,
|
|
767
|
+
location: S.Struct({
|
|
768
|
+
start: S.Struct({
|
|
769
|
+
line: S.Finite,
|
|
770
|
+
column: S.Finite
|
|
771
|
+
}),
|
|
772
|
+
end: S.Struct({
|
|
773
|
+
line: S.Finite,
|
|
774
|
+
column: S.Finite
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
}))
|
|
778
|
+
}))
|
|
779
|
+
});
|
|
780
|
+
/**
|
|
781
|
+
* Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
|
|
782
|
+
* `Left` is fatal by construction — it reaches the derived error channel and no write
|
|
783
|
+
* runs. A malformed report therefore never reaches the decider, and nothing here casts
|
|
784
|
+
* a third-party report type.
|
|
785
|
+
*/
|
|
786
|
+
const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
|
|
787
|
+
/**
|
|
788
|
+
* The prior report's facts the decision reads: its embedded configuration, which carries
|
|
789
|
+
* both the compared options and the survivors-run provenance marker, and the engine
|
|
790
|
+
* version it recorded. The report's files are not here — the survivors and the per-file
|
|
791
|
+
* source hashes derived from them need capabilities the command cannot hold, so they
|
|
792
|
+
* arrive already computed.
|
|
793
|
+
*/
|
|
794
|
+
var PriorReportFacts = class extends S.Class("PriorReportFacts")({
|
|
795
|
+
config: S.Record(S.String, S.Unknown),
|
|
796
|
+
frameworkVersion: S.UndefinedOr(S.String)
|
|
797
|
+
}) {};
|
|
798
|
+
/**
|
|
799
|
+
* The command of the admission workflow: a schema class, because `Workflow.make`
|
|
800
|
+
* constrains its first argument on the class value and a declared interface produces no
|
|
801
|
+
* value to pass. Every field is pure data — the two capabilities the previous shape
|
|
802
|
+
* carried, a digest function and a path resolver, can never be schema fields, so their
|
|
803
|
+
* results arrive precomputed from the decode phase instead.
|
|
804
|
+
*/
|
|
805
|
+
var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
|
|
806
|
+
/**
|
|
807
|
+
* The prior run's report facts, `undefined` when no report exists — the run cannot be
|
|
808
|
+
* admitted without one ('no-report'). Explicitly nullable rather than key-optional: a
|
|
809
|
+
* missing report is a state the edge determined and states, not a key it forgot.
|
|
810
|
+
*/
|
|
811
|
+
priorReport: S.UndefinedOr(PriorReportFacts),
|
|
812
|
+
/** The current run's resolved options (defaults + config file + CLI). */
|
|
813
|
+
currentConfig: S.Record(S.String, S.Unknown),
|
|
814
|
+
/** The current CLI/framework version (`strykerVersion`). */
|
|
815
|
+
frameworkVersion: S.String,
|
|
816
|
+
/**
|
|
817
|
+
* Per-file content hashes of the current source, keyed by the prior report's relative
|
|
818
|
+
* file keys. The prior side is hashed from the sources the report embeds, so an editor
|
|
819
|
+
* save that shifts line ranges — which would silently re-test a different mutant than
|
|
820
|
+
* the one that survived — is caught here.
|
|
821
|
+
*/
|
|
822
|
+
sourceContentHashes: S.Record(S.String, S.String),
|
|
823
|
+
/** The same hashes for the sources the prior report embeds, computed at the edge. */
|
|
824
|
+
priorSourceHashes: S.Record(S.String, S.String),
|
|
825
|
+
/** The prior report's survivors, already converted to the internal mutant shape. */
|
|
826
|
+
priorSurvivors: S.Array(MutantShape)
|
|
827
|
+
}) {};
|
|
732
828
|
const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
|
|
733
829
|
const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
|
|
734
830
|
const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
|
|
@@ -737,19 +833,23 @@ function priorSourceHashes(priorReport, hashContent) {
|
|
|
737
833
|
return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
|
|
738
834
|
}
|
|
739
835
|
/**
|
|
740
|
-
* Whether the admission
|
|
741
|
-
*
|
|
836
|
+
* Whether the admission inputs agree: the prior report's embedded resolved options,
|
|
837
|
+
* framework version and source content against the current run's.
|
|
838
|
+
*
|
|
839
|
+
* The comparison is on the canonical serializations rather than digests of them. Equal
|
|
840
|
+
* serializations are equal runs, so the digest was a lossy restatement of the check that
|
|
841
|
+
* also demanded a capability no command can carry.
|
|
742
842
|
*/
|
|
743
843
|
function hashesMatch(priorReport, input) {
|
|
744
|
-
return
|
|
844
|
+
return serializeSurvivorsHashInput({
|
|
745
845
|
resolvedOptions: stripSurvivorsKeys(priorReport.config),
|
|
746
|
-
frameworkVersion: priorReport.
|
|
747
|
-
sourceContentHashes:
|
|
748
|
-
}
|
|
846
|
+
frameworkVersion: priorReport.frameworkVersion,
|
|
847
|
+
sourceContentHashes: input.priorSourceHashes
|
|
848
|
+
}) === serializeSurvivorsHashInput({
|
|
749
849
|
resolvedOptions: stripSurvivorsKeys(input.currentConfig),
|
|
750
850
|
frameworkVersion: input.frameworkVersion,
|
|
751
851
|
sourceContentHashes: input.sourceContentHashes
|
|
752
|
-
}
|
|
852
|
+
});
|
|
753
853
|
}
|
|
754
854
|
const rejection = (reason, detail) => ({
|
|
755
855
|
kind: "reject",
|
|
@@ -760,31 +860,15 @@ function admissionVerdict(input) {
|
|
|
760
860
|
const priorReport = input.priorReport;
|
|
761
861
|
if (priorReport === void 0) return rejection("no-report", NO_REPORT_DETAIL);
|
|
762
862
|
if (wasProducedBySurvivorsRun(priorReport)) return rejection("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
|
|
763
|
-
|
|
764
|
-
if (survivors.length === 0) return { kind: "no-survivors" };
|
|
863
|
+
if (input.priorSurvivors.length === 0) return { kind: "no-survivors" };
|
|
765
864
|
if (!hashesMatch(priorReport, input)) return rejection("mismatch", MISMATCH_DETAIL);
|
|
766
865
|
return {
|
|
767
866
|
kind: "admit",
|
|
768
|
-
survivors
|
|
867
|
+
survivors: input.priorSurvivors
|
|
769
868
|
};
|
|
770
869
|
}
|
|
771
870
|
const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
|
|
772
|
-
var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(
|
|
773
|
-
id: S.String,
|
|
774
|
-
fileName: S.String,
|
|
775
|
-
mutatorName: S.String,
|
|
776
|
-
replacement: S.String,
|
|
777
|
-
location: S.Struct({
|
|
778
|
-
start: S.Struct({
|
|
779
|
-
line: S.Finite,
|
|
780
|
-
column: S.Finite
|
|
781
|
-
}),
|
|
782
|
-
end: S.Struct({
|
|
783
|
-
line: S.Finite,
|
|
784
|
-
column: S.Finite
|
|
785
|
-
})
|
|
786
|
-
})
|
|
787
|
-
})) }) {
|
|
871
|
+
var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
|
|
788
872
|
[SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
|
|
789
873
|
};
|
|
790
874
|
var NoSurvivors = class extends S.TaggedClass()("NoSurvivors", {}) {
|
|
@@ -804,7 +888,7 @@ var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
|
|
|
804
888
|
* the same reject outcome with different reasons; only the rejection's
|
|
805
889
|
* remediation names the full run to do first (R10).
|
|
806
890
|
*/
|
|
807
|
-
const admitSurvivorsRun = Workflow.make((command) => Match.value(admissionVerdict(command)).pipe(Match.discriminator("kind")("reject", (verdict) => Result.fail(SurvivorsRejection.make({
|
|
891
|
+
const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, (command) => Match.value(admissionVerdict(command)).pipe(Match.discriminator("kind")("reject", (verdict) => Result.fail(SurvivorsRejection.make({
|
|
808
892
|
reason: verdict.reason,
|
|
809
893
|
remediation: verdict.remediation
|
|
810
894
|
}))), Match.discriminator("kind")("no-survivors", () => Result.succeed(NoSurvivors.make())), Match.discriminator("kind")("admit", (verdict) => Result.succeed(Admitted.make({ survivors: verdict.survivors }))), Match.exhaustive));
|
|
@@ -812,10 +896,14 @@ const admitSurvivorsRun = Workflow.make((command) => Match.value(admissionVerdic
|
|
|
812
896
|
//#region src/SurvivorsExit.ts
|
|
813
897
|
/** The exit class a rejected survivors run exits with (R6: exit 2). */
|
|
814
898
|
const SURVIVORS_REJECT_EXIT_CLASS = ExitClass.ConfigError;
|
|
815
|
-
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/cli-request.schema.ts
|
|
901
|
+
const RunRequestSchema = S.TaggedStruct("run", {
|
|
816
902
|
options: S.Any,
|
|
817
903
|
survivors: S.Boolean
|
|
818
|
-
})
|
|
904
|
+
});
|
|
905
|
+
const LlmsRequestSchema = S.TaggedStruct("llms", { document: S.Any });
|
|
906
|
+
S.Union([RunRequestSchema, LlmsRequestSchema]);
|
|
819
907
|
//#endregion
|
|
820
908
|
//#region src/StrykerCliExecutor.ts
|
|
821
909
|
/**
|
|
@@ -866,48 +954,80 @@ function hostOptionsOf(mode, stream) {
|
|
|
866
954
|
* stashed context back and dispatches the decision to the verdict/run,
|
|
867
955
|
* failing the run with a rejection.
|
|
868
956
|
*/
|
|
869
|
-
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext) => pipe(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
957
|
+
const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext) => pipe(
|
|
958
|
+
Cell.read((cliOptions) => Effect.promise(() => resolveSurvivorsRunOptions(cliOptions)).pipe(Effect.flatMap((resolvedOptions) => {
|
|
959
|
+
const priorReportPath = priorReportPathOf(resolvedOptions);
|
|
960
|
+
const read = readPriorReport(priorReportPath);
|
|
961
|
+
return Ref.set(runContext, {
|
|
962
|
+
resolvedOptions,
|
|
963
|
+
priorReportPath
|
|
964
|
+
}).pipe(Effect.as({
|
|
965
|
+
resolvedOptions,
|
|
966
|
+
priorReportRaw: read.raw,
|
|
967
|
+
priorReportFound: read.found,
|
|
968
|
+
priorReportPath,
|
|
969
|
+
sourceContentHashes: currentSourceHashesFor(priorReportFileKeys(read.raw))
|
|
970
|
+
}));
|
|
971
|
+
}))),
|
|
972
|
+
/**
|
|
973
|
+
* The one place the prior report is decoded, and the one place the two capabilities
|
|
974
|
+
* are applied. A report that was never there yields a command with no facts, which
|
|
975
|
+
* the decider rejects as `no-report`; a report that was there and does not decode
|
|
976
|
+
* yields a `Left`, which stops the run before the decider sees it.
|
|
977
|
+
*/
|
|
978
|
+
Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
|
|
979
|
+
if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
|
|
980
|
+
priorReport: void 0,
|
|
981
|
+
currentConfig: resolvedOptions,
|
|
982
|
+
frameworkVersion: strykerVersion,
|
|
983
|
+
sourceContentHashes,
|
|
984
|
+
priorSourceHashes: {},
|
|
985
|
+
priorSurvivors: []
|
|
986
|
+
}));
|
|
987
|
+
return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
|
|
988
|
+
priorReport: PriorReportFacts.make({
|
|
989
|
+
config: document.config ?? {},
|
|
990
|
+
frameworkVersion: document.framework?.version
|
|
991
|
+
}),
|
|
992
|
+
currentConfig: resolvedOptions,
|
|
993
|
+
frameworkVersion: strykerVersion,
|
|
994
|
+
sourceContentHashes,
|
|
995
|
+
priorSourceHashes: priorSourceHashes(document, hashContent),
|
|
996
|
+
priorSurvivors: extractSurvivors(document, resolveAbsolutePath)
|
|
997
|
+
}));
|
|
998
|
+
}),
|
|
999
|
+
Cell.decide(admitSurvivorsRun),
|
|
1000
|
+
Cell.encode((outcome) => outcome),
|
|
1001
|
+
Cell.write((outcome) => Effect.flatMap(Ref.get(runContext), (context) => {
|
|
1002
|
+
if (context === void 0) return Effect.die("the survivors admission read must run before its write");
|
|
1003
|
+
const { resolvedOptions, priorReportPath } = context;
|
|
1004
|
+
return Result.match(outcome, {
|
|
1005
|
+
onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => Effect.sync(() => emitEmptySurvivorsVerdict(stream, mode, resolvedOptions))), Match.tag("Admitted", (admitted) => {
|
|
1006
|
+
const restricted = {
|
|
1007
|
+
...resolvedOptions,
|
|
1008
|
+
survivors: admitted.survivors,
|
|
1009
|
+
mutate: survivorMutateSpans(admitted.survivors),
|
|
1010
|
+
survivorsPriorReport: priorReportPath,
|
|
1011
|
+
incremental: false
|
|
1012
|
+
};
|
|
1013
|
+
return Effect.promise(() => runMutationTest(restricted));
|
|
1014
|
+
}), Match.orElse(() => Effect.die("unreachable admission decision variant"))),
|
|
1015
|
+
onFailure: (rejection) => Effect.fail(rejection)
|
|
1016
|
+
});
|
|
1017
|
+
}))
|
|
1018
|
+
);
|
|
905
1019
|
/**
|
|
906
1020
|
* The `--survivors` request: re-test exactly the prior report's survivor set.
|
|
907
1021
|
* The survivors flag was parsed as a boolean; the admission decides between
|
|
908
1022
|
* running the survivors and the plain pipeline. The chain's order is carried by
|
|
909
1023
|
* the description's phase types; the run's resolved context cell is created
|
|
910
1024
|
* here, beside the description it feeds.
|
|
1025
|
+
*
|
|
1026
|
+
* Two failures reach the caller, and they are not the same thing. A rejection is the
|
|
1027
|
+
* decision's own outcome — the run was inspected and refused. A `SchemaError` is a prior
|
|
1028
|
+
* report that was present and did not decode, which stops the chain before any decision
|
|
1029
|
+
* is made; it is in this signature because the phase types put it there, not because the
|
|
1030
|
+
* admission chose it.
|
|
911
1031
|
*/
|
|
912
1032
|
function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions) {
|
|
913
1033
|
return Effect.gen(function* () {
|
|
@@ -934,22 +1054,47 @@ function priorReportPathOf(resolved) {
|
|
|
934
1054
|
return typeof configured === "string" ? configured : DEFAULT_SURVIVORS_PRIOR_REPORT;
|
|
935
1055
|
}
|
|
936
1056
|
/**
|
|
937
|
-
*
|
|
938
|
-
*
|
|
1057
|
+
* Reads the prior report without validating it. Absence and malformation are different
|
|
1058
|
+
* outcomes and the caller must be able to tell them apart: an absent report is the
|
|
1059
|
+
* `no-report` rejection the decider states, while a present-but-malformed one is a decode
|
|
1060
|
+
* failure that stops the run. Text that is not JSON is reported as found, carrying the
|
|
1061
|
+
* text itself, so the codec refuses it and names what it got.
|
|
939
1062
|
*/
|
|
940
|
-
function isMutationTestResultShape(value) {
|
|
941
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
942
|
-
return "files" in value && typeof value.files === "object" && value.files !== null && !Array.isArray(value.files);
|
|
943
|
-
}
|
|
944
1063
|
function readPriorReport(priorReportPath) {
|
|
1064
|
+
let text;
|
|
945
1065
|
try {
|
|
946
|
-
|
|
947
|
-
const parsed = JSON.parse(raw);
|
|
948
|
-
return isMutationTestResultShape(parsed) ? parsed : void 0;
|
|
1066
|
+
text = readFileSync(priorReportPath, "utf-8");
|
|
949
1067
|
} catch {
|
|
950
|
-
return
|
|
1068
|
+
return {
|
|
1069
|
+
found: false,
|
|
1070
|
+
raw: void 0
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
return {
|
|
1075
|
+
found: true,
|
|
1076
|
+
raw: JSON.parse(text)
|
|
1077
|
+
};
|
|
1078
|
+
} catch {
|
|
1079
|
+
return {
|
|
1080
|
+
found: true,
|
|
1081
|
+
raw: text
|
|
1082
|
+
};
|
|
951
1083
|
}
|
|
952
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* The relative file names a report claims, read structurally rather than through the
|
|
1087
|
+
* codec because the current sources must be hashed before the report is decoded — the
|
|
1088
|
+
* read phase does the disk I/O, and the keys are what tell it which files to read. An
|
|
1089
|
+
* unrecognisable report yields no keys and is refused a phase later by the codec.
|
|
1090
|
+
*/
|
|
1091
|
+
function priorReportFileKeys(raw) {
|
|
1092
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
1093
|
+
if (!("files" in raw)) return [];
|
|
1094
|
+
const files = raw.files;
|
|
1095
|
+
if (typeof files !== "object" || files === null || Array.isArray(files)) return [];
|
|
1096
|
+
return Object.keys(files);
|
|
1097
|
+
}
|
|
953
1098
|
function readSourceFile(file) {
|
|
954
1099
|
try {
|
|
955
1100
|
return readFileSync(file, "utf-8");
|
|
@@ -958,15 +1103,13 @@ function readSourceFile(file) {
|
|
|
958
1103
|
}
|
|
959
1104
|
}
|
|
960
1105
|
/**
|
|
961
|
-
* The per-file content hashes of the current sources, keyed by the relative
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
* report embeds).
|
|
1106
|
+
* The per-file content hashes of the current sources, keyed by the relative file names
|
|
1107
|
+
* the prior report uses — the current side of the admission comparison. The prior side is
|
|
1108
|
+
* hashed from the sources the report embeds, in the decode phase.
|
|
965
1109
|
*/
|
|
966
|
-
function
|
|
1110
|
+
function currentSourceHashesFor(files) {
|
|
967
1111
|
const hashes = {};
|
|
968
|
-
|
|
969
|
-
for (const file of Object.keys(priorReport.files)) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
|
|
1112
|
+
for (const file of files) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
|
|
970
1113
|
return hashes;
|
|
971
1114
|
}
|
|
972
1115
|
/**
|
|
@@ -1145,10 +1288,16 @@ function carriesConfigError(cause) {
|
|
|
1145
1288
|
/**
|
|
1146
1289
|
* Classifies a failed run for the finalizer: usage/parse failures
|
|
1147
1290
|
* (`CliError` — except a bare help request, which exits 0), rejected
|
|
1148
|
-
* survivors runs (`SurvivorsRejection`)
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* classed code.
|
|
1291
|
+
* survivors runs (`SurvivorsRejection`), an unreadable prior report
|
|
1292
|
+
* (`S.SchemaError`) and a rejected config (`ConfigError`) all exit 2, all
|
|
1293
|
+
* other failures exit 1 (the framework's default). A successful run exits 0;
|
|
1294
|
+
* the verdict gates (U5) then resolve the final classed code.
|
|
1295
|
+
*
|
|
1296
|
+
* The report parse failure shares the survivors class deliberately. It is not a
|
|
1297
|
+
* verdict — the decider never sees the report — but the operator's answer is the
|
|
1298
|
+
* same class of answer as a rejection: the input you named cannot be used. Letting
|
|
1299
|
+
* it fall through to 1 would make an unusable `--survivors` input indistinguishable
|
|
1300
|
+
* from a crash.
|
|
1152
1301
|
*/
|
|
1153
1302
|
function resolveCliExitCode(exit) {
|
|
1154
1303
|
if (Exit.isSuccess(exit)) return 0;
|
|
@@ -1159,6 +1308,7 @@ function resolveCliExitCode(exit) {
|
|
|
1159
1308
|
if (S.is(CliError.ShowHelp)(value)) return value.errors.length > 0 ? 2 : 0;
|
|
1160
1309
|
if (CliError.isCliError(value)) return 2;
|
|
1161
1310
|
if (S.is(SurvivorsRejection)(value)) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
1311
|
+
if (value instanceof S.SchemaError) return SURVIVORS_REJECT_EXIT_CLASS;
|
|
1162
1312
|
}
|
|
1163
1313
|
if (carriesConfigError(exit.cause)) return ExitClass.ConfigError;
|
|
1164
1314
|
return 1;
|
|
@@ -1254,10 +1404,11 @@ const optional = (option) => Flag.optional(option);
|
|
|
1254
1404
|
/**
|
|
1255
1405
|
* Commander left an omitted flag out of the parsed options; `deepMerge` treats
|
|
1256
1406
|
* `undefined` as absent but an explicit `false` would override a config-file
|
|
1257
|
-
* `true`.
|
|
1258
|
-
* back to
|
|
1407
|
+
* `true`. `Flag.optional` yields `Option.none` when the flag is absent and
|
|
1408
|
+
* `Option.some(false)` for an explicit `--no-x`, so both map back to
|
|
1409
|
+
* `undefined` and leave the config-file default in force (KTD4).
|
|
1259
1410
|
*/
|
|
1260
|
-
const absentWhenFalse = (value) => value ? true : void 0;
|
|
1411
|
+
const absentWhenFalse = (value) => Option.isSome(value) && value.value ? true : void 0;
|
|
1261
1412
|
const LOG_LEVELS = [
|
|
1262
1413
|
"fatal",
|
|
1263
1414
|
"error",
|
|
@@ -1285,15 +1436,15 @@ function setLogLevel(target, key, value) {
|
|
|
1285
1436
|
}
|
|
1286
1437
|
const runOptions = {
|
|
1287
1438
|
ignorePatterns: Flag.string("ignorePatterns").pipe(Flag.withDescription("A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have NO effect when using the `--inPlace` option. The directories `node_modules`, `.git` and some others are always ignored. Example: `--ignorePatterns dist`. These patterns are ALWAYS ignored: [`node_modules`, `.git`, `/reports`, `*.tsbuildinfo`, `/stryker.log`, `.stryker-tmp`]. Because Stryker always ignores these, you should rarely have to adjust the `ignorePatterns` setting at all. This is useful to speed up Stryker by reducing the size of the sandbox directory which has a positive effect on performance."), Flag.map(splitOnComma), optional),
|
|
1288
|
-
ignoreStatic: Flag.map(Flag.boolean("ignoreStatic"), absentWhenFalse).pipe(Flag.withDescription("Ignore static mutants. Static mutants are mutants which are only executed during the loading of a file.")),
|
|
1289
|
-
incremental: Flag.map(Flag.boolean("incremental"), absentWhenFalse).pipe(Flag.withDescription("Enable 'incremental mode'. Stryker will store results in a file and use that file to speed up the next --incremental run")),
|
|
1290
|
-
allowEmpty: Flag.map(Flag.boolean("allowEmpty"), absentWhenFalse).pipe(Flag.withDescription("Allows stryker to exit without any errors in cases where no tests are found")),
|
|
1439
|
+
ignoreStatic: Flag.map(optional(Flag.boolean("ignoreStatic")), absentWhenFalse).pipe(Flag.withDescription("Ignore static mutants. Static mutants are mutants which are only executed during the loading of a file.")),
|
|
1440
|
+
incremental: Flag.map(optional(Flag.boolean("incremental")), absentWhenFalse).pipe(Flag.withDescription("Enable 'incremental mode'. Stryker will store results in a file and use that file to speed up the next --incremental run")),
|
|
1441
|
+
allowEmpty: Flag.map(optional(Flag.boolean("allowEmpty")), absentWhenFalse).pipe(Flag.withDescription("Allows stryker to exit without any errors in cases where no tests are found")),
|
|
1291
1442
|
incrementalFile: Flag.string("incrementalFile").pipe(Flag.withDescription("Specify the file to use for incremental mode."), optional),
|
|
1292
|
-
force: Flag.map(Flag.boolean("force"), absentWhenFalse).pipe(Flag.withDescription("Run all mutants, even if --incremental is provided and an incremental file exists. Can be used to force a rebuild of the incremental file.")),
|
|
1443
|
+
force: Flag.map(optional(Flag.boolean("force")), absentWhenFalse).pipe(Flag.withDescription("Run all mutants, even if --incremental is provided and an incremental file exists. Can be used to force a rebuild of the incremental file.")),
|
|
1293
1444
|
mutate: Flag.string("mutate").pipe(Flag.withAlias("m"), Flag.withDescription("With `mutate` you configure the subset of files or just one specific file to be mutated. These should be your _production code files_, and definitely not your test files. (Whereas with `ignorePatterns` you prevent non-relevant files from being copied to the sandbox directory in the first place)\nThe default will try to guess your production code files based on sane defaults. It reads like this:\n- Include all js-like files inside the `src` or `lib` dir\n- Except files inside `__tests__` directories and file names ending with `test` or `spec`.\nIf the defaults are not sufficient for you, for example in a angular project you might want to **exclude** not only the `*.spec.ts` files but other files too, just like the default already does.\nIt is possible to override the defaults by: - supplying one or more [glob patterns](https://github.com/isaacs/minimatch) to include (e.g. `src/**/*.js`) - or one or more comma separated glob patterns preceded with `!` to exclude (e.g. `!src/**/*.spec.js`) - or both (e.g. `src/**/*.js,!src/**/*.spec.js`).\nNote: Stryker will use [minimatch](https://github.com/isaacs/minimatch) for parsing these patterns, see minimatch for the exact syntax."), Flag.map(splitOnComma), optional),
|
|
1294
1445
|
testFiles: Flag.string("testFiles").pipe(Flag.withAlias("t"), Flag.withDescription("With `testFiles` you can limit which test files are executed during mutation testing. When specified, only tests from these files will be run. This allows you to verify that a module's dedicated unit tests can kill all its mutants independently."), Flag.map(splitOnComma), optional),
|
|
1295
1446
|
buildCommand: Flag.string("buildCommand").pipe(Flag.withAlias("b"), Flag.withDescription("Configure a build command to run after mutating the code, but before mutants are tested. This is generally used to transpile your code before testing. Only configure this if your test runner doesn't take care of this already and you're not using just-in-time transpiler like `babel/register` or `ts-node`."), optional),
|
|
1296
|
-
dryRunOnly: Flag.map(Flag.boolean("dryRunOnly"), absentWhenFalse).pipe(Flag.withDescription("Execute the initial test run only, without doing actual mutation testing. Doing a dry run only can be used to test that StrykerJS can run your test setup, for example, in CI pipelines.")),
|
|
1447
|
+
dryRunOnly: Flag.map(optional(Flag.boolean("dryRunOnly")), absentWhenFalse).pipe(Flag.withDescription("Execute the initial test run only, without doing actual mutation testing. Doing a dry run only can be used to test that StrykerJS can run your test setup, for example, in CI pipelines.")),
|
|
1297
1448
|
checkers: Flag.string("checkers").pipe(Flag.withDescription("A comma separated list of checkers to use, for example --checkers typescript"), Flag.map(splitOnComma), optional),
|
|
1298
1449
|
checkerNodeArgs: Flag.string("checkerNodeArgs").pipe(Flag.withDescription("A list of node args to be passed to checker child processes. Split on spaces (commander characterization): `--checkerNodeArgs \"--inspect-brk --trace-warnings\"`."), Flag.map(splitOnSpace), optional),
|
|
1299
1450
|
coverageAnalysis: Flag.choice("coverageAnalysis", [
|
|
@@ -1311,21 +1462,21 @@ const runOptions = {
|
|
|
1311
1462
|
dryRunTimeoutMinutes: Flag.float("dryRunTimeoutMinutes").pipe(Flag.withDescription("Configure an absolute timeout for the initial test run. (It can take a while.)"), optional),
|
|
1312
1463
|
maxConcurrentTestRunners: Flag.integer("maxConcurrentTestRunners").pipe(Flag.withDescription("Set the number of max concurrent test runner to spawn (default: cpuCount)"), optional),
|
|
1313
1464
|
concurrency: Flag.string("concurrency").pipe(Flag.withAlias("c"), Flag.withDescription("Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)"), Flag.map(parseConcurrency), optional),
|
|
1314
|
-
disableBail: Flag.map(Flag.boolean("disableBail"), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
|
|
1465
|
+
disableBail: Flag.map(optional(Flag.boolean("disableBail")), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
|
|
1315
1466
|
maxTestRunnerReuse: Flag.integer("maxTestRunnerReuse").pipe(Flag.withDescription("Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse."), optional),
|
|
1316
1467
|
logLevel: Flag.choice("logLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.logLevel}"`), optional),
|
|
1317
1468
|
fileLogLevel: Flag.choice("fileLogLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.fileLogLevel}"`), optional),
|
|
1318
|
-
inPlace: Flag.map(Flag.boolean("inPlace"), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
|
|
1469
|
+
inPlace: Flag.map(optional(Flag.boolean("inPlace")), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
|
|
1319
1470
|
tempDirName: Flag.string("tempDirName").pipe(Flag.withDescription("Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run"), optional),
|
|
1320
1471
|
cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${defaultOptions.tempDirName}" inside the current working directory by default) after a run.\n- false: Never delete the temp dir;\n- true: Delete the tmp dir after a successful run;\n- always: Always delete the temp dir, regardless of whether the run was successful.`), Flag.map(parseCleanDirOption), optional),
|
|
1321
|
-
survivors: Flag.map(Flag.boolean("survivors"), absentWhenFalse).pipe(Flag.withDescription("Re-run only the mutants that survived a previous run. Admits against the previous run's mutation report (the `survivorsPriorReport` config option, default `reports/mutation-report.json`) and re-tests exactly the survivor set. Exits 2 with a remediation naming a full run when the report is missing, drifted, or the configuration changed; exits 0 with a null score when the report has no survivors."))
|
|
1472
|
+
survivors: Flag.map(optional(Flag.boolean("survivors")), absentWhenFalse).pipe(Flag.withDescription("Re-run only the mutants that survived a previous run. Admits against the previous run's mutation report (the `survivorsPriorReport` config option, default `reports/mutation-report.json`) and re-tests exactly the survivor set. Exits 2 with a remediation naming a full run when the report is missing, drifted, or the configuration changed; exits 0 with a null score when the report has no survivors."))
|
|
1322
1473
|
};
|
|
1323
1474
|
const runArgs = { configFile: Argument.optional(Argument.string("configFile")) };
|
|
1324
1475
|
const runConfig = {
|
|
1325
1476
|
...runOptions,
|
|
1326
1477
|
...runArgs
|
|
1327
1478
|
};
|
|
1328
|
-
const rootConfig = { llms: Flag.map(Flag.boolean("llms"), absentWhenFalse).pipe(Flag.withDescription("Print the agent-facing command manifest as one JSON object on stdout: every option, alias, kind, default, allowed value set and description, plus the subcommands and positional arguments, walked from the command descriptors.")) };
|
|
1479
|
+
const rootConfig = { llms: Flag.map(optional(Flag.boolean("llms")), absentWhenFalse).pipe(Flag.withDescription("Print the agent-facing command manifest as one JSON object on stdout: every option, alias, kind, default, allowed value set and description, plus the subcommands and positional arguments, walked from the command descriptors.")) };
|
|
1329
1480
|
function unwrap(value) {
|
|
1330
1481
|
if (Option.isOption(value)) return Option.match(value, {
|
|
1331
1482
|
onNone: () => void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/stryker-js-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@effect/platform-node": "^4.0.0-rc.
|
|
18
|
-
"@effect/platform-node-shared": "^4.0.0-rc.
|
|
19
|
-
"
|
|
20
|
-
"effect": "4.0.0-rc.108",
|
|
17
|
+
"@effect/platform-node": "^4.0.0-rc.111",
|
|
18
|
+
"@effect/platform-node-shared": "^4.0.0-rc.111",
|
|
19
|
+
"effect": "4.0.0-rc.111",
|
|
21
20
|
"semver": "^7.7.0",
|
|
22
|
-
"@systemfsoftware/stryker-js-
|
|
23
|
-
"@systemfsoftware/
|
|
24
|
-
"@systemfsoftware/stryker-js-mutation-
|
|
25
|
-
"@systemfsoftware/stryker-js-
|
|
21
|
+
"@systemfsoftware/stryker-js-plugin-api": "^2.1.0",
|
|
22
|
+
"@systemfsoftware/stryker-js-mutation-run": "^4.0.0",
|
|
23
|
+
"@systemfsoftware/stryker-js-mutation-report": "^1.2.8",
|
|
24
|
+
"@systemfsoftware/stryker-js-util": "^0.1.0",
|
|
25
|
+
"@systemfsoftware/effect-cell-types": "^4.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"tsdown": "^0.22.14",
|
|
36
36
|
"vite-tsconfig-paths": "^6.1.1",
|
|
37
37
|
"vitest": "^4.1.10",
|
|
38
|
-
"@systemfsoftware/effect-gherkin-spec": "^2.0.
|
|
39
|
-
"@systemfsoftware/effect-schema-law": "^0.
|
|
40
|
-
"@systemfsoftware/oxlint-plugin-cell-vocabulary": "^1.2.0",
|
|
41
|
-
"@systemfsoftware/effect-schema-vite": "^1.5.3",
|
|
38
|
+
"@systemfsoftware/effect-gherkin-spec": "^2.0.1",
|
|
39
|
+
"@systemfsoftware/effect-schema-law": "^0.9.0",
|
|
42
40
|
"@systemfsoftware/oxlint-config": "^0.1.0",
|
|
41
|
+
"@systemfsoftware/effect-schema-vite": "^1.5.4",
|
|
43
42
|
"@systemfsoftware/tsconfig": "^1.3.3",
|
|
44
|
-
"@systemfsoftware/oxlint-plugin-effect-entrypoint": "^1.0.
|
|
43
|
+
"@systemfsoftware/oxlint-plugin-effect-entrypoint": "^1.0.5",
|
|
45
44
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
46
|
-
"@systemfsoftware/oxlint-plugin-test-placement": "^3.
|
|
45
|
+
"@systemfsoftware/oxlint-plugin-test-placement": "^3.1.0",
|
|
46
|
+
"@systemfsoftware/oxlint-plugin-cell-vocabulary": "^1.2.1"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=20.0.0"
|