test_gates 0.2.1
test_gates: ^0.2.1 copied to clipboard
Test gates for the Dart and Flutter code that must not be wrong: per-file 100% line and branch coverage, static checks and mutation testing.
Changelog #
0.2.1 - 2026-10-13 #
For dart_mutant 0.2.
mutationworks withdart_mutant0.2.0 and every later 0.2.x, and no longer with 0.1.x (dart pub global activate dart_mutant '^0.2.0'). Whatmutationrelies on is the same in both: the arguments ofrun, the plan file, the report, the exit codes and the output of--version. One range is named, and it is the one of the version that runs the mutants of a pure Dart package faster. Install 0.2 on every machine that runsmutation, and in CI;test_gates0.2.0 stays the version fordart_mutant0.1.x.- What changes with
dart_mutant0.2.0 is in its changelog. For a run ofmutation: the tests of a gate of a pure Dart package are compiled once for its mutants where that can be done, the lines it prints per gate say so, and the gates are no longer changed in place with one job. They still are in a package that cannot be run from a copy, and with one job where every mutant gets a start of the test runner of its own (a Flutter package,-- --one-by-one --jobs 1).-- --one-by-oneruns the mutants as 0.1 ran them. - The message that says that
dart_mutantwas not found names the supported versions in the command that installs it (dart pub global activate dart_mutant '^0.2.0'), as the message for a version that is not supported does. Without the range a later version than the supported ones could be installed.
0.2.0 - 2026-10-12 #
test_gates mutation no longer makes the mutants and runs the tests itself.
It starts dart_mutant for that, as
the npm package starts Stryker, and judges the report. check follows the
imports of a gate, and a new command, timezones, runs the tests of the
gates in other time zones. The README has the steps under "Upgrading from
0.1".
Breaking #
mutation:
mutationneedsdart_mutant0.1.1 or a later 0.1.x (dart pub global activate dart_mutant; Dart 3.11 or later). It is looked for onPATH, then asdart pub global run dart_mutant. When it is missing, or is not one of those versions, the exit code is 2 and nothing is run. 0.1.0 is not supported: an experimental option of it could report a mutant as killed that survives. The other commands run on Dart 3.9 as before.- What follows
--on the command line ofmutationis handed todart_mutant run(--jobs <n>,--shard <i>/<n>,--html <file>). It used to be the command that runs test files; that command now goes behind a second--(mutation -- -- fvm flutter test --no-pub), and the old form is refused with exit code 2.--dir,--plan,--reportand--onlyare refused behind--:mutationsets them.--helpand--versionare refused there as well. - More mutants, so new survivors are to be expected. The twelve mutators of
0.1 make the same mutants as before, and an
equivalentMutantsorexpectedTimeoutsentry written for 0.1 still matches.dart_mutantalso has mutators that use resolved types (MethodExpression,NumberLiteral,ConstantNumberLiteral,GuardAlwaysTaken,GuardThrows,GuardResultSwapped,BranchCopied,ReturnValue). A survivor of those fails the run like any other until a test detects it or the gate allows it with a reason. - The exit code of
mutationcan be one ofdart_mutant: 2 when it refused its command line, and whatever else it ended with other than 0 and 1 (137 when the system killed it). Nothing is judged then. mutation-resultrefuses the report of a part of a run (dart_mutant run --shard) as a whole. Such a report hasconfig.shard.- The results of the last run are those of
dart_mutant(.dart_tool/dart_mutant/)..dart_tool/test_gates/mutation/of 0.1 is no longer read and can be deleted; a mutant that a killed run of 0.1 left in a gate is not put back by 0.2.0. - The lines
mutationprinted per gate are now printed bydart_mutant, in its format, and the report namesdart_mutantas itsframework.
check and test-gates.json:
checkfollows the imports of a gate. The imports and exports that point into the package are followed, file by file, and what a file on the way imports, reads from the environment or matches ofsettings.forbiddenSourceis a violation of the gate, reported with the file, the line and the files in between. A gate that passed because the impure import was one file away now fails. A file that is to be left out, with what is behind it, goes intosettings.impureImports.allow. A file on the way that cannot be read is a violation as well.checkreports an import whose URI is not written plainly (an upper-case scheme, a%,file:): Dart readsDART:ioasdart:io, and the rules compare the text. An import that leaves the package (../..above its root) is reported as a file that cannot be read, and apubspec.yamlwhosenameis not found is a violation, becausepackage:<name>/could then not be followed.- More packages are impure by default:
go_router,auto_route,cached_network_image,google_fonts(UI);file,process,platform(files, processes, the environment);connectivity_plus,device_info_plus,package_info_plus,share_plus,battery_plus,sensors_plus,network_info_plus,url_launcher,permission_handler,geolocator,image_picker,file_picker,local_auth(the device);socket_io_client,mqtt_client(network);get_storage,realm,floor(persistence);hydrated_bloc,replay_bloc,state_notifier,signals,async_redux,watch_it,stacked(state management);supabase,appwrite,googleapis,google_sign_in,in_app_purchase(services), andrealm_dart,dart_appwrite,googleapis_auth,signals_core(the Dart-only packages of four of those).package:fileandpackage:platformare also what a pure function takes a file system or a platform as;settings.impureImports.allowexempts them, as it exempts any other. - A key that an entry of
equivalentMutants/expectedTimeoutsdoes not have is a violation (exit code 1), and the entry is not used. A misspeltoccurrencewas read as an entry without one. - A pattern of
settings.forbiddenSource.addis tested against the code of a line. What a comment or a doc comment says no longer matches; the text of a string literal still does.
Added #
test_gates timezones [--zone <name>] [--all] [-- <command>]: runs the tests of the gates once more in other time zones (theTZenvironment variable; macOS and Linux), so that a decision about a day, a month or an hour does not pass because of where the machine is. By default in the two ends of the clocks,Pacific/Kiritimati(UTC+14) andEtc/GMT+12(UTC-12): at every moment the date in one of them is another one than in UTC. Only gates that nameDateTime(in the gate, in its test, or in a file of the package the two reach) are run; when none does, no test is started. A zone that is not an entry of the time zone database of the machine is refused.test_gates all [--selfcheck] [--force] [--no-mutation]:check,test,timezonesandmutationin one start of the tool, one after the other, until the first that fails; its exit code is the one ofall. Each check is the command of that name, with its output and its verdict. What it saves is the start of the tool for every check but the first.test --selfcheck: the run of the tests thattestjudges is also the negative control ofselfcheck, with a probe in every gate. One run of the test suite in place of two. The entries of the probes (DA:,BRDA:, with function coverageFN:/FNDA:, and the totalsLF:/FNF:) are taken out of the tracefile before it is judged and left behind; what is left is the tracefile the command writes without probes.mutation --not-evaluableandmutation-result --not-evaluablelist the mutants that could not be evaluated, with the message of the compiler when the report has it. Without the option they are counted, as before.checksays in a note when there is nopubspec.yamland a gate haspackage:imports, or reaches a file that has: without the name of the package they are taken for imports of another package, and are not followed.
Changed #
mutationwrites the gates and their tests as a plan file (.dart_tool/test_gates/mutation-plan.json) and startsdart_mutant run --dir <package> --plan <plan> --report <report>, with--forcefor--forceand--only <path>for each--gate <path>. The test command is the onedart_mutantchooses (flutter test --no-pubordart test);settings.gateCommandis not used for it.- The verdict is that of the report, not of the exit code of
dart_mutant. With 0 or 1 the report is judged asmutation-resultjudges it. A gate that is not in the report fails, and so does exit code 1 with a report in which no mutant of the gates of the run survived and none is missing. - Before the tool is started, what an earlier report says about the gates of
the run is taken out of it, so that a run that writes nothing is not judged
on old results. A file at that place that is not a mutation report is no
longer deleted (0.1 deleted it);
dart_mutantrefuses to replace it. mutation -- --shard <i>/<n>runs a part and does not judge it: exit code 0 when the part was written with every gate of the run in it. The parts are put together withdart_mutant mergeand judged withmutation-result.- A signal (SIGINT, SIGTERM, SIGHUP) is handed to
dart_mutantand to every process that descends from it, and they are waited for (60 seconds at the most) while the tool puts back what it changed. When the tool does not end in that time and is killed, the message says so, and that a gate may still have a mutant in it. - With two jobs or more (
dart_mutantdecides;-- --jobs <n>), the gates are not changed in place: the mutants are put into working copies. check,lcovandmutation-resultend with a line and exit code 1 when a file cannot be read (a gate without read permission). They ended with an unhandled error.mutationstarted from a compiled executable of the tool looks fordart pub global run dart_mutantwith thedartonPATH.- When
dart_mutantis not found, the message says what each command that was tried did: that it could not be started, or its exit code and the first line it printed. lcov: the parser no longer trims the fields of aDA:/BRDA:/LF:/LH:entry before it reads them as numbers. Blanks around a number are read as before; the calls did nothing.
Removed #
- The mutation engine of 0.1: making the mutants, changing the gates in
place, the test runs, the time limits, the report writer, the file of
results of the last run and the recovery of a killed run. They are
dart_mutant's now.
0.1.1 - 2026-10-11 #
Documentation only; the commands behave as in 0.1.0.
- The outputs and paths shown in the README are those of the package in
example/, which now has two gates, a candidate and an allowed equivalent mutant.
0.1.0 - 2026-10-11 #
First version. The Dart port of
@t09tanaka/test-gates 0.3.2: the
same test-gates.json, with the same keys and the same rules for reading it.
Unknown keys are rejected in the same places. The built-in defaults are those
of Dart (settings.gateExtensions: [".dart"], the test of a gate, the
impure imports).
- The command line is the interface. The library
(
package:test_gates/test_gates.dart) has that command line as a function and nothing else:runAsync,IoandpackageVersion. What is underlib/src/is not part of the interface. test_gates lcov [--file <lcov.info>]: every gate must be at 100% line coverage and 100% branch coverage. A gate without a record, a gate without an instrumented line, a tracefile collected without--branch-coverage, and a tracefile with an unreadable entry or an unclosed last record fail.- The JSON Schema ships in the package (
schema/test-gates.schema.json). test_gates mutation [--force] [--report <path>] [-- <command>]: mutation test of the gates. The mutants are made from the syntax tree (package:analyzer), each is put in the gate, and the test of the gate is run on it (flutter test --no-pubfor a Flutter package,dart testotherwise, with--fail-fast --timeout 10s). The statuses areKilled,Survived,TimeoutandCompileError.- Mutators, named as in Stryker where it has the same one:
ArithmeticOperator,EqualityOperator,LogicalOperator(with??),ConditionalExpression,BooleanLiteral,UnaryOperator,UpdateOperator,AssignmentOperator,BlockStatement,RemoveCall,StringLiteral,ArrayDeclaration. No mutator can be switched off and no comment hides a mutant. - Mutants that the syntax shows cannot compile are not made. The others
that do not compile are counted as
CompileErrorand shown in the summary (not evaluable); they do not fail the run. - The limits are fixed: a test gets 10 seconds, the test run of a mutant
three times the run without a mutant plus 20 seconds (only this one is a
Timeout), the run without a mutant 10 minutes. - A gate whose source, tests, test command and tool version are those of
the last run is not run again (
.dart_tool/test_gates/mutation/), except for its timeouts. Files the gate imports are not looked at:--forceruns everything, and is what CI should use. - The gates are changed in place, one mutant at a time, by renaming a complete file over the gate, and put back after every test run, on an error and on SIGINT / SIGTERM / SIGHUP. What a killed run leaves is put back by the next run from a backup; a file that was edited in between is never overwritten.
--gate <path>(may be given more than once) runs and judges only that gate, for working on one gate and for one CI job per gate. A path that is not a gate is wrong usage. The report keeps what it says about the other gates, and so do the results of the last run, with--forceas well. The last line says how many gates were judged.mutation-result --gate <path>judges the same gates of a report.
- Mutators, named as in Stryker where it has the same one:
test_gates mutation-result [--report <path>]: judges a report in the mutation-testing-elements (Stryker) JSON schema with the rules of upstream'smutation-result: a survivor needs exactly oneequivalentMutantsentry (mutator+original+replacement,occurrencewhen ambiguous,reason), a stale allowance fails, a timeout outsideexpectedTimeoutsfails abovesettings.mutation.maxTimeouts(default 0), a gate without mutants fails.equivalentMutants,expectedTimeouts,settings.mutation.maxTimeoutsandsettings.stryker.reportFileare those of upstream. The report is written where upstream's key says (defaultreports/mutation/mutation.json), so that onetest-gates.jsonis accepted by both tools.- A command that is ended (by a signal, or by the time limit of a mutant) is
ended together with the processes that descend from it, found with
ps. This also holds fortestandselfcheck. On Windows, and where there is nops, only the command itself is ended. test_gates selfcheck [--all | --first] [-- <command>]: negative control. Appends code that no test executes to every gate, runs the tests with coverage once, and requires the tracefile to report that very line as not executed and not taken for each gate. The gates are changed in place, only by appending, and put back when the run ends, on an error and on SIGINT / SIGTERM / SIGHUP; a leftover of a killed run failslcovand is removed by the nextselfcheck.test_gates test [-- <command>]: runs the tests with coverage and then judges the tracefile aslcovdoes. A tracefile of an earlier run is deleted first.settings.gateCommand: the command that runs the tests and writes the tracefile, as an array, started without a shell (on Windows with one, asflutteranddartare batch files there).settings.selfcheck.modesets the default of--all/--first.settings.selfcheck.failurePatternis accepted and has no effect.test_gates check: the static checks of upstream, for Dart. The gates are parsed withpackage:analyzer(>=8.2.0 <15.0.0), not matched with patterns, so an annotation such as@JsonSerializable()is not a problem.- The list: a gate exists (case-sensitive), has a gate extension, is a plain relative path, is not a test or a generated file; candidates exist.
- The test:
lib/a/b.dartneedslib/a/b_test.dartortest/a/b_test.dart, exactly one.settings.spec.suffixesandsettings.spec.rewritechange that;"suffixes": []turns the test checks off. The test has noskip:/solo:/@Skip/markTestSkipped(). - Imports, exports and parts: Flutter,
dart:io,dart:ui,dart:ffi,dart:isolate,dart:mirrors, the browser libraries, HTTP clients, persistence, state management and service SDKs are rejected by default (settings.impureImports).part 'x.g.dart'andpart 'x.freezed.dart'are allowed, any other part is not.settings.imports.mode: "allowlist"turns the check around. - Source: no read of the environment (
Platform.environment,String.fromEnvironment, ...;settings.forbiddenSource), no coverage ignore comment, no syntax error. - Dart only: no two blocks start on the same line. They would share one branch entry in lcov, and the untested one would not be reported.
settings.impureImports,impureNamedImports,impurePaths,forbiddenSource,importAliasesandimportsare read, with defaults for Dart. The keys are those of upstream.settings.spechas a default for Dart as well (see above).