strictcli 0.41.2__tar.gz → 0.42.0__tar.gz
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.
- {strictcli-0.41.2 → strictcli-0.42.0}/PKG-INFO +3 -3
- {strictcli-0.41.2 → strictcli-0.42.0}/README.md +1 -1
- {strictcli-0.41.2 → strictcli-0.42.0}/pyproject.toml +2 -2
- {strictcli-0.41.2 → strictcli-0.42.0}/strictcli/__init__.py +57 -35
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_coverage.py +127 -23
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_effects.py +6 -3
- {strictcli-0.41.2 → strictcli-0.42.0}/uv.lock +1 -1
- {strictcli-0.41.2 → strictcli-0.42.0}/.claude/settings.json +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.gitignore +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.rlsbl/config.json +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/CLAUDE.md +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/LICENSE +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/scripts/add_effect_classification.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/scripts/add_forwarding_declaration.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/scripts/migrate_presence.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/strictcli/py.typed +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/conftest.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/flagship_app.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_arg_decorator_order.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_argv_value_order.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_call.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_command.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_provider.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_check_types.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_choice_records.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_choices.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_choices_none.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_claimed_rendering.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_classification.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_config.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_confirm.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_connection_env.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_constraints.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_context.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_dry_run_unsupported.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_e2e.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_effects_bypass_check.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_env.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_flagship_preview.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_flat_pre_typed.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_float_format.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_float_type.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_float_vectors.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_guard_v2.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_help.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_hermetic.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_infra_env.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_int_type.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_invoke.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_machine_mode.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_mcp.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_member_spelling.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_nesting.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_owns_stdout.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_parser.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_payload_schema.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_presence.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_provenance.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_record_pre_typed.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_registration.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_reserved_quartet.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_selectors.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_trace_store.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_ulid_vectors.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_unique.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_update.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_utilities.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_validate.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_value_sweep_order.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_variadic.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/tests/test_visibility.py +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.41.2 → strictcli-0.42.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: strictcli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A
|
|
3
|
+
Version: 0.42.0
|
|
4
|
+
Summary: A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation)
|
|
5
5
|
Project-URL: Homepage, https://smmh.dev/strictcli/
|
|
6
6
|
Project-URL: Documentation, https://smmh.dev/strictcli/
|
|
7
7
|
Project-URL: Repository, https://github.com/smm-h/strictcli
|
|
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
# strictcli
|
|
26
26
|
|
|
27
|
-
A
|
|
27
|
+
A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation).
|
|
28
28
|
|
|
29
29
|
strictcli makes you declare everything -- every command, flag, argument, and environment variable must have help text or the framework errors at registration time. Four types only: `str`, `bool`, `int`, `float`. No magic type inference, no implicit defaults.
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# strictcli
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation).
|
|
4
4
|
|
|
5
5
|
strictcli makes you declare everything -- every command, flag, argument, and environment variable must have help text or the framework errors at registration time. Four types only: `str`, `bool`, `int`, `float`. No magic type inference, no implicit defaults.
|
|
6
6
|
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "strictcli"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "A
|
|
7
|
+
version = "0.42.0"
|
|
8
|
+
description = "A CLI framework for the Era of Agents: nothing is inferred, everything is declared. First-class support for Go, Python, and TypeScript (Python implementation)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.42.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet",
|
|
@@ -8724,6 +8724,17 @@ def warn_check_spec(
|
|
|
8724
8724
|
)
|
|
8725
8725
|
|
|
8726
8726
|
|
|
8727
|
+
def _msg_test_coverage_boolean_retired() -> str:
|
|
8728
|
+
"""The retired boolean's refusal (contract §12.12: one sentence, each
|
|
8729
|
+
language's own spellings inside it -- `test_coverage` / `test_coverage_dir`
|
|
8730
|
+
here, `WithTestCoverage` / `WithTestCoverageDir` in Go, `testCoverage` /
|
|
8731
|
+
`testCoverageDir` in TypeScript)."""
|
|
8732
|
+
return (
|
|
8733
|
+
"test_coverage is not accepted; declare the directory holding "
|
|
8734
|
+
"coverage/ and test-coverage.json with test_coverage_dir"
|
|
8735
|
+
)
|
|
8736
|
+
|
|
8737
|
+
|
|
8727
8738
|
@dataclass
|
|
8728
8739
|
class App:
|
|
8729
8740
|
"""The root CLI application."""
|
|
@@ -8761,13 +8772,25 @@ class App:
|
|
|
8761
8772
|
proc_observe_allowlist: list[list[str]] | None = None
|
|
8762
8773
|
checks_path: str | Path | None = None
|
|
8763
8774
|
checks_embed: bytes | None = None
|
|
8764
|
-
|
|
8775
|
+
# The directory holding this app's coverage state: `coverage/` (per-process
|
|
8776
|
+
# shard files) and `test-coverage.json` (the committed manifest). Declared,
|
|
8777
|
+
# never discovered. Absent means coverage is off -- no provider registered,
|
|
8778
|
+
# no paths computed, nothing touched. A declared directory that does not
|
|
8779
|
+
# exist at construction likewise leaves coverage off: that is the installed
|
|
8780
|
+
# distribution, where the path naming the source checkout is simply gone.
|
|
8781
|
+
test_coverage_dir: str | os.PathLike | None = None
|
|
8782
|
+
# Refusal-only. The retired boolean has no accepted spelling; this field
|
|
8783
|
+
# exists so that passing it names the option that replaced it instead of
|
|
8784
|
+
# raising CPython's bare "unexpected keyword argument" TypeError.
|
|
8785
|
+
test_coverage: bool | None = None
|
|
8765
8786
|
flags: list[Flag] = field(default_factory=list)
|
|
8766
8787
|
_commands: dict[str, Command] = field(default_factory=dict)
|
|
8767
8788
|
_groups: dict[str, Group] = field(default_factory=dict)
|
|
8768
8789
|
_deprecated: dict[str, DeprecatedCommand] = field(default_factory=dict)
|
|
8769
8790
|
|
|
8770
8791
|
def __post_init__(self) -> None:
|
|
8792
|
+
if self.test_coverage is not None:
|
|
8793
|
+
raise ValueError(_msg_test_coverage_boolean_retired())
|
|
8771
8794
|
_require_non_empty_str(self.version, "version", "App")
|
|
8772
8795
|
_require_non_empty_str(self.help, "help", "App")
|
|
8773
8796
|
# Check for duplicate and reserved global flag names
|
|
@@ -8973,28 +8996,27 @@ class App:
|
|
|
8973
8996
|
self._coverage_dir: str | None = None
|
|
8974
8997
|
self._coverage_manifest_path: str | None = None
|
|
8975
8998
|
self._last_resolved_path: list[str] = []
|
|
8976
|
-
if self.
|
|
8977
|
-
#
|
|
8978
|
-
#
|
|
8979
|
-
#
|
|
8980
|
-
#
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
os.path.join(
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
self.register_check_provider(self._test_coverage_provider)
|
|
8999
|
+
if self.test_coverage_dir is not None:
|
|
9000
|
+
# The declared directory decides everything. An app installed
|
|
9001
|
+
# elsewhere finds it absent and stays uninstrumented: no provider,
|
|
9002
|
+
# no paths, no writes anywhere -- in particular not into whatever
|
|
9003
|
+
# directory the consumer happened to start the CLI from.
|
|
9004
|
+
root = os.path.abspath(os.fspath(self.test_coverage_dir))
|
|
9005
|
+
if os.path.isdir(root):
|
|
9006
|
+
# Only the PATHS are computed here. The coverage directory
|
|
9007
|
+
# itself is created lazily by _record_coverage, immediately
|
|
9008
|
+
# before the first shard write: shards are written only on the
|
|
9009
|
+
# test-harness paths (test() and call()), so a plain CLI
|
|
9010
|
+
# invocation leaves no coverage/ behind.
|
|
9011
|
+
self._coverage_dir = os.path.join(root, "coverage")
|
|
9012
|
+
self._coverage_manifest_path = os.path.join(
|
|
9013
|
+
root, "test-coverage.json"
|
|
9014
|
+
)
|
|
9015
|
+
self._coverage_shard_path = os.path.join(
|
|
9016
|
+
self._coverage_dir,
|
|
9017
|
+
f"{os.getpid()}.jsonl",
|
|
9018
|
+
)
|
|
9019
|
+
self.register_check_provider(self._test_coverage_provider)
|
|
8998
9020
|
|
|
8999
9021
|
def _validate_flag_infra_marker(self, f: Flag) -> None:
|
|
9000
9022
|
"""Panic if a flag's default is a RelativeToRoot marker referencing an
|
|
@@ -9074,19 +9096,19 @@ class App:
|
|
|
9074
9096
|
def _test_coverage_provider(self) -> list[CheckSpec]:
|
|
9075
9097
|
"""Built-in check provider for cli-test-coverage.
|
|
9076
9098
|
|
|
9077
|
-
Registered automatically when
|
|
9078
|
-
from committed state: the covered set is
|
|
9079
|
-
manifest (
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9099
|
+
Registered automatically when test_coverage_dir names a directory that
|
|
9100
|
+
exists. The verdict is derived from committed state: the covered set is
|
|
9101
|
+
the union of the committed manifest (test-coverage.json in that
|
|
9102
|
+
directory) and any per-process shard files merged from its coverage/
|
|
9103
|
+
subdirectory. Every live registered command path (minus the injected
|
|
9104
|
+
check command) must be present in that union to pass; otherwise the
|
|
9105
|
+
check fails naming each uncovered command.
|
|
9083
9106
|
|
|
9084
9107
|
Because the verdict reads the committed manifest, it is deterministic on
|
|
9085
9108
|
every machine -- a machine that never ran the suite (no local shards)
|
|
9086
9109
|
still gets a stable verdict from the committed manifest alone. Both the
|
|
9087
|
-
coverage dir and the manifest path
|
|
9088
|
-
|
|
9089
|
-
the app's own repo state.
|
|
9110
|
+
coverage dir and the manifest path sit under the DECLARED directory, so
|
|
9111
|
+
the check evaluated from any cwd reads the app's own repo state.
|
|
9090
9112
|
|
|
9091
9113
|
The manifest is rewritten as the monotonic union of its prior contents
|
|
9092
9114
|
and the freshly merged shards, but ONLY when that content actually
|
|
@@ -11515,7 +11537,7 @@ class App:
|
|
|
11515
11537
|
self._begin_dispatch()
|
|
11516
11538
|
cmd_path = ".".join(self._last_resolved_path + [cmd.name])
|
|
11517
11539
|
# Record test-coverage hit (command-level only, test mode only).
|
|
11518
|
-
if mode == "test" and self.
|
|
11540
|
+
if mode == "test" and self._coverage_shard_path is not None:
|
|
11519
11541
|
self._record_coverage(cmd_path)
|
|
11520
11542
|
# Store sources for function handlers that need provenance info
|
|
11521
11543
|
self._last_sources = sources
|
|
@@ -11782,7 +11804,7 @@ class App:
|
|
|
11782
11804
|
|
|
11783
11805
|
self._begin_dispatch()
|
|
11784
11806
|
# Record test-coverage hit (command-level only).
|
|
11785
|
-
if self.
|
|
11807
|
+
if self._coverage_shard_path is not None:
|
|
11786
11808
|
self._record_coverage(command_path)
|
|
11787
11809
|
|
|
11788
11810
|
# Passthrough commands: forward raw args to the passthrough handler
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Tests for the cli-test-coverage mechanism.
|
|
2
2
|
|
|
3
3
|
Verifies that:
|
|
4
|
-
-
|
|
5
|
-
- test() and call() both record to per-process shard files
|
|
4
|
+
- test_coverage_dir=<existing dir> enables recording of command hits
|
|
5
|
+
- test() and call() both record to per-process shard files under that directory
|
|
6
6
|
- The cli-test-coverage check merges shards, compares against the command
|
|
7
7
|
surface, and FAILs listing uncovered commands
|
|
8
8
|
- Full coverage produces a PASS
|
|
9
9
|
- Empty/stale manifest is a hard error
|
|
10
|
+
- A declared directory that does not exist leaves the instrumentation off
|
|
11
|
+
- The retired boolean is refused by name
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import json
|
|
@@ -24,12 +26,18 @@ class SimpleCtx:
|
|
|
24
26
|
project_root: Path
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def _coverage_root(tmp_path):
|
|
30
|
+
"""The declared coverage directory, created so the option takes effect."""
|
|
31
|
+
root = tmp_path / ".strictcli"
|
|
32
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return root
|
|
34
|
+
|
|
35
|
+
|
|
27
36
|
def _make_app(tmp_path):
|
|
28
|
-
"""Build a 3-command app
|
|
29
|
-
os.chdir(tmp_path)
|
|
37
|
+
"""Build a 3-command app whose coverage directory is tmp_path/.strictcli."""
|
|
30
38
|
app = strictcli.App(
|
|
31
39
|
name="coverapp", version="1.0.0", help="coverage test app",
|
|
32
|
-
|
|
40
|
+
test_coverage_dir=str(_coverage_root(tmp_path)),
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
@app.command(name="deploy", effect="read_only", forwarding=strictcli.Forwarding(reason="test handler absorbs global flag values"), help="deploy the app")
|
|
@@ -50,10 +58,9 @@ def _make_app(tmp_path):
|
|
|
50
58
|
|
|
51
59
|
def _make_grouped_app(tmp_path):
|
|
52
60
|
"""Build an app with grouped commands for dotted-path coverage."""
|
|
53
|
-
os.chdir(tmp_path)
|
|
54
61
|
app = strictcli.App(
|
|
55
62
|
name="grpapp", version="1.0.0", help="grouped coverage test",
|
|
56
|
-
|
|
63
|
+
test_coverage_dir=str(_coverage_root(tmp_path)),
|
|
57
64
|
)
|
|
58
65
|
|
|
59
66
|
grp = app.group("infra", help="infrastructure commands")
|
|
@@ -273,20 +280,20 @@ class TestCoverageCheck:
|
|
|
273
280
|
|
|
274
281
|
|
|
275
282
|
class TestCoverageChdirSafety:
|
|
276
|
-
def
|
|
277
|
-
"""test() records into the
|
|
278
|
-
|
|
279
|
-
app = _make_app(tmp_path) #
|
|
283
|
+
def test_record_anchored_to_declared_directory(self, tmp_path):
|
|
284
|
+
"""test() records into the declared coverage dir, not the cwd that a
|
|
285
|
+
test happened to chdir into."""
|
|
286
|
+
app = _make_app(tmp_path) # declares tmp_path/.strictcli
|
|
280
287
|
other = tmp_path / "elsewhere"
|
|
281
288
|
other.mkdir()
|
|
282
289
|
os.chdir(other)
|
|
283
290
|
|
|
284
291
|
app.test(["deploy"])
|
|
285
292
|
|
|
286
|
-
|
|
293
|
+
declared_shards = list(
|
|
287
294
|
(tmp_path / ".strictcli" / "coverage").glob("*.jsonl")
|
|
288
295
|
)
|
|
289
|
-
assert
|
|
296
|
+
assert declared_shards, "shard must be written under the declared dir"
|
|
290
297
|
foreign = other / ".strictcli" / "coverage"
|
|
291
298
|
assert not foreign.exists(), "must not record into the chdir'd cwd"
|
|
292
299
|
|
|
@@ -311,7 +318,7 @@ class TestManifestUnionVerdict:
|
|
|
311
318
|
|
|
312
319
|
def test_check_from_foreign_cwd_reads_app_state(self, tmp_path):
|
|
313
320
|
"""The check evaluated from a foreign cwd reads the app's own repo state
|
|
314
|
-
(
|
|
321
|
+
(the declared manifest), not the foreign directory."""
|
|
315
322
|
app = _make_app(tmp_path)
|
|
316
323
|
manifest_path = tmp_path / ".strictcli" / "test-coverage.json"
|
|
317
324
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -364,7 +371,7 @@ class TestManifestUnionVerdict:
|
|
|
364
371
|
|
|
365
372
|
class TestCoverageDisabled:
|
|
366
373
|
def test_no_recording_when_disabled(self, tmp_path):
|
|
367
|
-
"""
|
|
374
|
+
"""An undeclared test_coverage_dir (the default) produces no shards."""
|
|
368
375
|
os.chdir(tmp_path)
|
|
369
376
|
app = strictcli.App(
|
|
370
377
|
name="nocover", version="1.0.0", help="no coverage",
|
|
@@ -381,13 +388,13 @@ class TestCoverageDisabled:
|
|
|
381
388
|
|
|
382
389
|
|
|
383
390
|
class TestCoverageDirectoryIsLazy:
|
|
384
|
-
def
|
|
385
|
-
"""Constructing an app with
|
|
386
|
-
|
|
387
|
-
not plant an empty directory
|
|
391
|
+
def test_construction_leaves_no_coverage_subdirectory(self, tmp_path):
|
|
392
|
+
"""Constructing an app with a declared coverage directory must not
|
|
393
|
+
create coverage/ inside it -- a plain CLI invocation never records
|
|
394
|
+
coverage, so it must not plant an empty directory."""
|
|
388
395
|
app = _make_app(tmp_path)
|
|
389
396
|
assert app is not None
|
|
390
|
-
assert not (tmp_path / ".strictcli").exists()
|
|
397
|
+
assert not (tmp_path / ".strictcli" / "coverage").exists()
|
|
391
398
|
|
|
392
399
|
def test_recording_creates_the_directory(self, tmp_path):
|
|
393
400
|
"""The recorder creates the coverage directory immediately before the
|
|
@@ -398,11 +405,11 @@ class TestCoverageDirectoryIsLazy:
|
|
|
398
405
|
shard = tmp_path / ".strictcli" / "coverage" / f"{os.getpid()}.jsonl"
|
|
399
406
|
assert shard.is_file()
|
|
400
407
|
|
|
401
|
-
def
|
|
402
|
-
"""The provider reads a
|
|
408
|
+
def test_check_skips_when_coverage_state_absent(self, tmp_path):
|
|
409
|
+
"""The provider reads a declared root holding no coverage state without
|
|
403
410
|
raising -- it reports the subject-matter SKIP."""
|
|
404
411
|
app = _make_app(tmp_path)
|
|
405
|
-
assert not (tmp_path / ".strictcli").exists()
|
|
412
|
+
assert not (tmp_path / ".strictcli" / "coverage").exists()
|
|
406
413
|
|
|
407
414
|
results, _, _code = app.run_checks(
|
|
408
415
|
SimpleCtx(project_root=tmp_path),
|
|
@@ -410,3 +417,100 @@ class TestCoverageDirectoryIsLazy:
|
|
|
410
417
|
)
|
|
411
418
|
cov_result = next(r for r in results if r.name == "cli-test-coverage")
|
|
412
419
|
assert cov_result.status == "skip"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class TestDeclaredDirectoryAbsent:
|
|
423
|
+
"""A declared directory that does not exist leaves coverage off.
|
|
424
|
+
|
|
425
|
+
This is the installed-wheel case: the path that names the source
|
|
426
|
+
checkout's ``.strictcli`` directory is simply not there once the CLI is
|
|
427
|
+
installed elsewhere, so the app registers no check, computes no paths and
|
|
428
|
+
creates nothing.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def test_missing_directory_registers_no_check(self, tmp_path):
|
|
432
|
+
missing = tmp_path / "nowhere" / ".strictcli"
|
|
433
|
+
app = strictcli.App(
|
|
434
|
+
name="coverapp", version="1.0.0", help="coverage test app",
|
|
435
|
+
test_coverage_dir=str(missing),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
@app.command(name="deploy", effect="read_only", help="deploy the app")
|
|
439
|
+
def cmd_deploy(ctx):
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
# No provider, so the check system never turns on: there is no `check`
|
|
443
|
+
# command to route to, and no cli-test-coverage to list.
|
|
444
|
+
result = app.test(["check", "--all"])
|
|
445
|
+
assert result.exit_code == 1
|
|
446
|
+
assert "cli-test-coverage" not in result.stdout
|
|
447
|
+
with pytest.raises(ValueError, match="checks are not enabled"):
|
|
448
|
+
app.run_checks(SimpleCtx(project_root=tmp_path), run_all=True)
|
|
449
|
+
|
|
450
|
+
def test_missing_directory_creates_nothing(self, tmp_path):
|
|
451
|
+
missing = tmp_path / "nowhere" / ".strictcli"
|
|
452
|
+
app = strictcli.App(
|
|
453
|
+
name="coverapp", version="1.0.0", help="coverage test app",
|
|
454
|
+
test_coverage_dir=str(missing),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
@app.command(name="deploy", effect="read_only", help="deploy the app")
|
|
458
|
+
def cmd_deploy(ctx):
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
app.test(["deploy"])
|
|
462
|
+
app.call("deploy")
|
|
463
|
+
|
|
464
|
+
assert not (tmp_path / "nowhere").exists()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class TestForeignDirectoryRun:
|
|
468
|
+
"""A consumer-style run from a foreign directory.
|
|
469
|
+
|
|
470
|
+
The installed CLI is started in some unrelated project. Its declared
|
|
471
|
+
coverage directory does not exist there, so `check` must neither list
|
|
472
|
+
cli-test-coverage nor write a `.strictcli/` into the foreign directory.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
def test_foreign_run_lists_no_check_and_touches_nothing(self, tmp_path):
|
|
476
|
+
foreign = tmp_path / "some-other-project"
|
|
477
|
+
foreign.mkdir()
|
|
478
|
+
os.chdir(foreign)
|
|
479
|
+
|
|
480
|
+
app = strictcli.App(
|
|
481
|
+
name="coverapp", version="1.0.0", help="coverage test app",
|
|
482
|
+
test_coverage_dir=str(tmp_path / "gone" / ".strictcli"),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
@app.command(name="deploy", effect="read_only", help="deploy the app")
|
|
486
|
+
def cmd_deploy(ctx):
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
result = app.test(["check", "--all"])
|
|
490
|
+
assert "cli-test-coverage" not in result.stdout
|
|
491
|
+
|
|
492
|
+
assert not (foreign / ".strictcli").exists()
|
|
493
|
+
assert list(foreign.iterdir()) == []
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TestRetiredBooleanRefused:
|
|
497
|
+
def test_boolean_true_is_refused_naming_the_directory_option(self):
|
|
498
|
+
with pytest.raises(ValueError) as exc:
|
|
499
|
+
strictcli.App(
|
|
500
|
+
name="coverapp", version="1.0.0", help="coverage test app",
|
|
501
|
+
test_coverage=True,
|
|
502
|
+
)
|
|
503
|
+
assert str(exc.value) == (
|
|
504
|
+
"test_coverage is not accepted; declare the directory holding "
|
|
505
|
+
"coverage/ and test-coverage.json with test_coverage_dir"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def test_boolean_false_is_refused_too(self):
|
|
509
|
+
"""There is no accepted spelling of the retired option, not even the
|
|
510
|
+
one that used to mean "off" -- absence is how coverage is declared off."""
|
|
511
|
+
with pytest.raises(ValueError) as exc:
|
|
512
|
+
strictcli.App(
|
|
513
|
+
name="coverapp", version="1.0.0", help="coverage test app",
|
|
514
|
+
test_coverage=False,
|
|
515
|
+
)
|
|
516
|
+
assert "test_coverage_dir" in str(exc.value)
|
|
@@ -1147,7 +1147,8 @@ class TestCacheWrites:
|
|
|
1147
1147
|
|
|
1148
1148
|
def test_cache_writes_never_appear_in_the_would_do_log(self, tmp_path, monkeypatch):
|
|
1149
1149
|
monkeypatch.chdir(tmp_path)
|
|
1150
|
-
|
|
1150
|
+
(tmp_path / ".strictcli").mkdir()
|
|
1151
|
+
app = _app(test_coverage_dir=str(tmp_path / ".strictcli"))
|
|
1151
1152
|
|
|
1152
1153
|
@app.command("run", help="run", effect="read_only")
|
|
1153
1154
|
def _run(ctx):
|
|
@@ -1166,7 +1167,8 @@ class TestCacheWrites:
|
|
|
1166
1167
|
would move user-visible numbering for no visible reason.
|
|
1167
1168
|
"""
|
|
1168
1169
|
monkeypatch.chdir(tmp_path)
|
|
1169
|
-
|
|
1170
|
+
(tmp_path / ".strictcli").mkdir()
|
|
1171
|
+
app = _app(test_coverage_dir=str(tmp_path / ".strictcli"))
|
|
1170
1172
|
|
|
1171
1173
|
@app.command("rel", help="rel", effect="mutating")
|
|
1172
1174
|
def _rel(ctx):
|
|
@@ -1189,7 +1191,8 @@ class TestCacheWrites:
|
|
|
1189
1191
|
def test_cache_writes_do_not_shift_the_truncation_step(self, tmp_path,
|
|
1190
1192
|
monkeypatch):
|
|
1191
1193
|
monkeypatch.chdir(tmp_path)
|
|
1192
|
-
|
|
1194
|
+
(tmp_path / ".strictcli").mkdir()
|
|
1195
|
+
app = _app(test_coverage_dir=str(tmp_path / ".strictcli"))
|
|
1193
1196
|
|
|
1194
1197
|
@app.command("rel", help="rel", effect="mutating")
|
|
1195
1198
|
def _rel(ctx):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|