diff --git a/NOTICE b/NOTICE index 18574fa..cd2f253 100644 --- a/NOTICE +++ b/NOTICE @@ -173,3 +173,42 @@ pgregory.net/rapid is licensed under the Mozilla Public License, Version 2.0. It is a test-only dependency: internal/proptest contains only _test.go files, so rapid does not appear in this module's non-test import graph and is not built into any consumer that imports this library. + +The marker/ package's conformance tests port the marker evaluation and parse +cases of pypa/packaging (https://github.com/pypa/packaging), Copyright (c) +Donald Stufft and individual contributors, specifically tests/test_markers.py's +classes TestOperatorEvaluation and TestMarker and its module-level and/or +evaluation tests, pinned at commit 4eb0753dba8fcaaac8eb75463374e448f0931558 +(identical at release 26.3), licensed under the Apache License, Version 2.0. +Translated from Python/pytest to Go table-driven tests, in +marker/conformance_test.go. Upstream tests resting on implementation details +with no counterpart here are not ported, and the reason is recorded in the +file's header: TestNode, TestDefaultEnvironment (the live interpreter), +__str__/__repr__/__hash__/__eq__, the operator-overload tests, every +test_pickle_*, evaluate(context=...), and the set-valued "extras"/ +"dependency_groups" marker variables (PEP 685/735), which this package's +grammar does not recognize at all. Where a Go expectation differs from +upstream's literal assertion the divergence is annotated inline at its table. + +The extras/ package's conformance tests port the name-canonicalization cases +of pypa/packaging (https://github.com/pypa/packaging), Copyright (c) Donald +Stufft and individual contributors, specifically tests/test_utils.py's +test_canonicalize_name and the non-validating half of +test_canonicalize_name_invalid, pinned at commit +4eb0753dba8fcaaac8eb75463374e448f0931558, licensed under the Apache License, +Version 2.0. Translated from Python/pytest to Go table-driven tests, in +extras/conformance_test.go. + +The internal/pep508 package's error-position conformance tests port input +strings and caret span offsets - not message text - from Astral uv's +uv-pep508 crate (https://github.com/astral-sh/uv), specifically +crates/uv-pep508/src/lib.rs's `mod tests` error_* cases, pinned at commit +01cb90c1a4f88af09906cb60de9766d2add4a062 (release 0.12.18). uv-pep508 +overrides uv's workspace license to Apache-2.0 OR BSD-2-Clause (Copyright (c) +2023 konstin) and ships its own license files; this material is taken under +the Apache-2.0 arm. Translated from Rust/insta snapshots to Go table-driven +tests, in internal/pep508/error_position_test.go, asserting this package's own +byte offsets and message text throughout - never uv's prose, and never any +case behind uv's "non-pep508-extensions" cargo feature (unnamed +direct-reference requirements, which this module's grammar has no +counterpart for). diff --git a/extras/conformance_test.go b/extras/conformance_test.go new file mode 100644 index 0000000..a8bd076 --- /dev/null +++ b/extras/conformance_test.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Portions of this file port test cases from pypa/packaging +// (https://github.com/pypa/packaging), specifically tests/test_utils.py's +// test_canonicalize_name, used under the Apache License, Version 2.0 +// (dual-licensed Apache-2.0 OR BSD-2-Clause; see NOTICE for full license and +// copyright detail). +// Changed: translated from Python/pytest to Go table-driven tests; see the +// divergence notes below for cases where the Go expectation intentionally +// differs from upstream's literal assertion. +// +// Of test_utils.py's 11 test functions, only this one maps cleanly onto this +// module: 6 are parse_wheel_filename/parse_sdist_filename (wheelname/'s +// concern, and parse_sdist_filename has no Go counterpart at all), and +// test_canonicalize_version, test_is_normalized_name, and the validating half +// of canonicalize_name(validate=True) have no Go implementation anywhere in +// this module. Only extras.Normalize (canonicalize_name) is ported here. +// +// Upstream pinned at 4eb0753dba8fcaaac8eb75463374e448f0931558 (identical at +// release 26.3: the only test_markers.py/test_utils.py change since is a type +// annotation, not a case). + +package extras + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Ported from pypa/packaging tests/test_utils.py, test_canonicalize_name. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L23-L39 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_utils.py#L23-L39 +func TestConformance_CanonicalizeName(t *testing.T) { + tests := []struct { + name string + want string + }{ + {"foo", "foo"}, + {"Foo", "foo"}, + {"fOo", "foo"}, + {"foo.bar", "foo-bar"}, + {"Foo.Bar", "foo-bar"}, + {"Foo.....Bar", "foo-bar"}, + {"foo_bar", "foo-bar"}, + {"foo___bar", "foo-bar"}, + {"foo-bar", "foo-bar"}, + {"foo----bar", "foo-bar"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, Normalize(tt.name)) + }) + } +} + +// Ported from pypa/packaging tests/test_utils.py, +// test_canonicalize_name_invalid - the NON-VALIDATING half only +// (`assert canonicalize_name(name) == expected`). The validating half +// (`canonicalize_name(name, validate=True)` raising InvalidName) has no +// counterpart: Normalize has no validate mode, and PEP 503/685 name-validity +// checking is not implemented anywhere in this module. A half-port, not a +// skip: the assertion this function still makes is real and ported as-is. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L42-L57 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_utils.py#L42-L57 +func TestConformance_CanonicalizeNameInvalid_NonValidatingHalf(t *testing.T) { + tests := []struct { + name string + want string + }{ + {"_not_legal", "-not-legal"}, + {"hi\n", "hi\n"}, + {"\nhi", "\nhi"}, + {"h\ni", "h\ni"}, + {"hi\r", "hi\r"}, + {"\rhi", "\rhi"}, + {"h\ri", "h\ri"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, Normalize(tt.name)) + }) + } +} diff --git a/internal/pep508/error_position_test.go b/internal/pep508/error_position_test.go new file mode 100644 index 0000000..d28c69d --- /dev/null +++ b/internal/pep508/error_position_test.go @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Portions of this file port test inputs and caret positions from Astral uv's +// uv-pep508 crate (https://github.com/astral-sh/uv), specifically +// crates/uv-pep508/src/lib.rs's tests module (the error_* tests), used under +// the Apache License, Version 2.0 (uv-pep508 is dual-licensed Apache-2.0 OR +// BSD-2-Clause, Copyright (c) 2023 konstin; see NOTICE). +// Changed: only inputs and span offsets are ported; message text is this +// module's own, not uv's. uv-pep508's own test module says "Half of these +// tests are copied from https://github.com/pypa/packaging/pull/624". +// +// Pinned: 01cb90c1a4f88af09906cb60de9766d2add4a062 (release 0.12.18). Scope, +// per the issue: nothing from uv-pep440, nothing from uv-pep508/src/marker/, +// only the `mod tests` block of crates/uv-pep508/src/lib.rs's error_* tests - +// 43 of the 44, excluding error_invalid_extra_unnamed_url (behind uv's +// "non-pep508-extensions" cargo feature, covering unnamed direct-reference +// requirements with no counterpart in this module's grammar). +// +// # The caret-translation rule +// +// uv's snapshot draws a span as a run of "^" characters under the covered +// display columns, e.g. `numpy >=1.1.*` / ` ^^^^^^^`. Ours renders +// SyntaxError as Msg + "\n" + Source + "\n" + spaces(Start) + "~"*(End-Start) +// + "^" (tokenizer.go). uv's span [start, start+width) in DISPLAY COLUMNS is +// translated to bytes here by measuring the same input through Go's own +// rune/byte indexing - the two agree everywhere except error_unicode_after_extra +// (see its row below), because uv counts display columns (unicode-width) and +// we count bytes. +// +// # Why 5 pass, 30 are annotated divergences, and 10 are skips +// +// Every "Expected X" raised by this grammar (NewSyntaxError/expect) is a +// ZERO-WIDTH point at the parser's cursor - the position where the missing +// token would have started. uv instead spans the FULL offending token. Where +// our point coincides with uv's span START, that is annotated below as a +// divergence (not a bug: our Start is exactly where uv's span begins, we just +// do not additionally measure a width). Where the anchor itself differs - an +// unclosed bracket/paren reported at end-of-input here vs. at the opening +// delimiter by uv, or a uv-only "unnamed URL" heuristic pypa has no +// counterpart for - that is a skip: reproducing uv's anchor would mean +// widening the zero-width convention across many call sites, well past a +// small, contained fix. Four more are skips because pypa (our semantics +// anchor) and uv disagree on the grammar itself, or because the input's +// defect is version-string validity, which this raw grammar entry point does +// not check at all (that happens one layer up, in package requirement, via +// version.NewSpecifiers). +package pep508 + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// uvCase is one ported uv-pep508 error_* (or, for the two non-"error_"-named +// upstream functions, parse_name_with_star / test_error_invalid_marker_key) +// input. uvStart/uvEnd is uv's own span, translated to bytes per the file +// header. skip, when non-empty, is a documented skip reason and the row's +// wantStart/wantEnd/wantMsg are left unset (the loop skips before asserting). +type uvCase struct { + name string + input string + uvStart, uvEnd int + skip string + + wantStart, wantEnd int + wantMsg string +} + +const ( + skipUnclosedDelimiterAnchor = "different anchor: this grammar reports the cursor position where the closing delimiter was expected (end of input); uv reports the position of the opening delimiter that never closed. Reproducing uv's anchor would mean widening the zero-width error convention across every unclosed-bracket/paren call site, past a small fix." + skipOracleAcceptsTrailingUnderscore = `oracle rule: pypa accepts a trailing "_" in this identifier grammar (same production for both a package name and an extra name); uv rejects it. We follow pypa, the semantics anchor.` + skipVersionValidityDeferred = "version-string validity (a wildcard/operator mismatch, an invalid PEP 440 local segment) is checked one layer up, in package requirement via version.NewSpecifiers - not by this raw grammar entry point, which accepts any specifier-shaped text." +) + +var uvCases = []uvCase{ + // --- pass: our zero-width point lands at the SAME byte offset uv's + // span both starts AND ends at (uv's own span is itself zero-width here: + // each points one past the last real character, at end-of-input). --- + { + name: "error_marker_incomplete1", input: `numpy; sys_platform`, + uvStart: 19, uvEnd: 19, + wantStart: 19, wantEnd: 19, + wantMsg: "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in", + }, + { + name: "error_marker_incomplete2", input: `numpy; sys_platform ==`, + uvStart: 22, uvEnd: 22, + wantStart: 22, wantEnd: 22, + wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_marker_incomplete3", input: `numpy; sys_platform == "win32" or`, + uvStart: 33, uvEnd: 33, + wantStart: 33, wantEnd: 33, + wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_marker_incomplete5", input: `numpy; sys_platform == "win32" or (os_name == "linux" and`, + uvStart: 57, uvEnd: 57, + wantStart: 57, wantEnd: 57, + wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_name_at_nothing", input: `name @`, + uvStart: 6, uvEnd: 6, + wantStart: 6, wantEnd: 6, + wantMsg: "Expected URL after '@'", + }, + + // --- annotated divergence: our zero-width point equals uv's span + // START; we do not additionally measure the offending token's width. --- + { + name: "error_empty", input: ``, + uvStart: 0, uvEnd: 0, // uv: "Empty field is not allowed for PEP508", no span at all + wantStart: 0, wantEnd: 0, wantMsg: "Expected package name", + }, + { + name: "error_start", input: `_name`, + uvStart: 0, uvEnd: 1, + wantStart: 0, wantEnd: 0, wantMsg: "Expected package name", + }, + { + name: "error_no_name", input: `==0.0`, + uvStart: 0, uvEnd: 1, + wantStart: 0, wantEnd: 0, wantMsg: "Expected package name", + }, + { + name: "error_unnamed_file_path", input: `/path/to/flask.tar.gz`, + uvStart: 0, uvEnd: 21, + wantStart: 0, wantEnd: 0, wantMsg: "Expected package name", + }, + { + name: "error_extras_illegal_start1", input: `black[ö]`, + uvStart: 6, uvEnd: 8, + wantStart: 6, wantEnd: 6, wantMsg: "Expected ']'", + }, + { + name: "error_extras_illegal_start2", input: `black[_d]`, + uvStart: 6, uvEnd: 7, + wantStart: 6, wantEnd: 6, wantMsg: "Expected ']'", + }, + { + name: "error_extras_illegal_start3", input: `black[,]`, + uvStart: 6, uvEnd: 7, + wantStart: 6, wantEnd: 6, wantMsg: "Expected ']'", + }, + { + name: "error_extras_illegal_character", input: `black[jüpyter]`, + uvStart: 7, uvEnd: 9, + wantStart: 7, wantEnd: 7, wantMsg: "Expected ']'", + }, + { + name: "error_extras_illegal_end/dash", input: `foo[bar-]`, + uvStart: 7, uvEnd: 8, + wantStart: 7, wantEnd: 7, wantMsg: "Expected ']'", + }, + { + name: "error_extras_illegal_end/dot", input: `foo[bar.]`, + uvStart: 7, uvEnd: 8, + wantStart: 7, wantEnd: 7, wantMsg: "Expected ']'", + }, + { + // uv counts display columns (unicode-width), we count bytes: 'α' is + // one display column but two UTF-8 bytes. Here that only widens uv's + // END (our zero-width point still lands at Start=8, the same byte + // offset uv's span begins at, since nothing multi-byte precedes it) - + // annotated, not "fixed": see the file header's caret-translation rule. + name: "error_unicode_after_extra", input: "foo[bar α]", + uvStart: 8, uvEnd: 10, + wantStart: 8, wantEnd: 8, wantMsg: "Expected ']'", + }, + { + name: "error_extra_with_trailing_comma", input: `black[d,]`, + uvStart: 8, uvEnd: 9, + wantStart: 8, wantEnd: 8, wantMsg: "Expected extra name after ','", + }, + { + name: "error_parenthesized_pep440", input: `numpy ( ><1.19 )`, + uvStart: 8, uvEnd: 15, + wantStart: 8, wantEnd: 8, wantMsg: "Expected ')'", + }, + { + name: "error_whats_that", input: `numpy % 1.16`, + uvStart: 6, uvEnd: 7, + wantStart: 6, wantEnd: 6, wantMsg: "Expected end of requirement string", + }, + { + name: "error_no_comma_between_extras", input: `name[bar baz]`, + uvStart: 9, uvEnd: 10, + wantStart: 9, wantEnd: 9, wantMsg: "Expected ']'", + }, + { + name: "error_extra_comma_after_extras", input: `name[bar, baz,]`, + uvStart: 14, uvEnd: 15, + wantStart: 14, wantEnd: 14, wantMsg: "Expected extra name after ','", + }, + { + name: "error_extras_not_closed", input: `name[bar, baz >= 1.0`, + uvStart: 14, uvEnd: 15, + wantStart: 14, wantEnd: 14, wantMsg: "Expected ']'", + }, + { + name: "parse_name_with_star/dash", input: `wheel-*.whl`, + uvStart: 5, uvEnd: 6, + wantStart: 5, wantEnd: 5, wantMsg: "Expected end of requirement string", + }, + { + // Same byte-vs-display-column note as error_unicode_after_extra: 'Ѧ' + // is one display column, two UTF-8 bytes, widening only uv's End. + name: "parse_name_with_star/non_ascii", input: "wheelѦ", + uvStart: 5, uvEnd: 7, + wantStart: 5, wantEnd: 5, wantMsg: "Expected end of requirement string", + }, + { + name: "test_error_invalid_marker_key", input: `name; invalid_name`, + uvStart: 6, uvEnd: 18, + wantStart: 6, wantEnd: 6, wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_markers_invalid_order", input: `name; '3.7' <= invalid_name`, + uvStart: 15, uvEnd: 27, + wantStart: 15, wantEnd: 15, wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_markers_notin", input: `name; '3.7' notin python_version`, + uvStart: 12, uvEnd: 17, + wantStart: 12, wantEnd: 12, + wantMsg: "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in", + }, + { + name: "error_missing_quote", input: `name; python_version == 3.10`, + uvStart: 24, uvEnd: 28, + wantStart: 24, wantEnd: 24, wantMsg: "Expected a marker variable or quoted string", + }, + { + name: "error_non_ascii_after_marker", input: `foo; python_version == "3.12" αx`, + uvStart: 30, uvEnd: 33, + wantStart: 30, wantEnd: 30, wantMsg: "Expected end of requirement string", + }, + { + name: "error_markers_inpython_version", input: `name; '3.6'inpython_version`, + uvStart: 11, uvEnd: 27, + wantStart: 11, wantEnd: 11, + wantMsg: "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in", + }, + { + name: "error_markers_not_python_version", input: `name; '3.7' not python_version`, + uvStart: 16, uvEnd: 17, + wantStart: 16, wantEnd: 16, wantMsg: "Expected 'in' after 'not'", + }, + { + name: "error_markers_invalid_operator", input: `name; '3.7' ~ python_version`, + uvStart: 12, uvEnd: 13, + wantStart: 12, wantEnd: 12, + wantMsg: "Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in", + }, + { + name: "error_no_version_value", input: `name==`, + uvStart: 4, uvEnd: 6, + wantStart: 4, wantEnd: 4, wantMsg: "Expected end of requirement string", + }, + { + name: "error_no_version_operator", input: `name 1.0`, + uvStart: 5, uvEnd: 6, + wantStart: 5, wantEnd: 5, wantMsg: "Expected end of requirement string", + }, + + // --- annotated divergence, individually: our point sits at the LAST + // byte of uv's span, not its first (both anchor the same "#" character; + // uv also spans the whitespace and specifier text before it). --- + { + name: "error_random_char", input: `name >= 1.0 #`, + uvStart: 5, uvEnd: 13, + wantStart: 12, wantEnd: 12, wantMsg: "Expected end of requirement string", + }, + + // --- skip: different anchor entirely (unclosed delimiter) --- + { + name: "error_extras_eof1", input: `black[`, + uvStart: 5, uvEnd: 6, + skip: skipUnclosedDelimiterAnchor, + }, + { + name: "error_extras_eof2", input: `black[d`, + uvStart: 5, uvEnd: 6, + skip: skipUnclosedDelimiterAnchor, + }, + { + name: "error_extras_eof3", input: `black[d,`, + uvStart: 5, uvEnd: 6, + skip: skipUnclosedDelimiterAnchor, + }, + { + name: "error_parenthesized_parenthesis", input: `numpy ( >=1.19`, + uvStart: 6, uvEnd: 7, + skip: skipUnclosedDelimiterAnchor, + }, + { + name: "error_marker_incomplete4", input: `numpy; sys_platform == "win32" or (os_name == "linux"`, + uvStart: 34, uvEnd: 35, + skip: skipUnclosedDelimiterAnchor, + }, + + // --- skip: uv-only heuristic with no pypa counterpart --- + { + name: "error_unnamedunnamed_url", input: `git+https://github.com/pallets/flask.git`, + uvStart: 0, uvEnd: 40, + skip: `uv detects an unnamed direct-reference URL as a special case with a tailored "add a package name" message; pypa (our semantics anchor) has no such heuristic, and neither do we. Ours simply fails parsing the truncated identifier "git" as a package name followed by unexpected input - correct per PEP 508 (a bare URL is not a valid name_req), but a plainer diagnostic. Implementing uv's heuristic would be new behavior, not a bug fix.`, + }, + + // --- skip: oracle conflict (pypa and uv disagree on the grammar) --- + { + name: "error_end", input: `name_`, + uvStart: 4, uvEnd: 5, + skip: skipOracleAcceptsTrailingUnderscore, + }, + { + name: "error_extras_illegal_end/trailing_underscore", input: `foo[bar_]`, + uvStart: 7, uvEnd: 8, + skip: skipOracleAcceptsTrailingUnderscore, + }, + + // --- skip: version-string validity is out of this entry point's scope --- + { + name: "error_pep440", input: `numpy >=1.1.*`, + uvStart: 6, uvEnd: 13, + skip: skipVersionValidityDeferred, + }, + { + name: "error_invalid_prerelease", input: `name==1.0.org1`, + uvStart: 4, uvEnd: 14, + skip: skipVersionValidityDeferred, + }, +} + +// TestConformance_UVErrorPositions asserts, for every in-scope uv-pep508 +// error_* input, the byte Start/End our own *SyntaxError reports and its +// rendered Error() text - never uv's message text (see file header). A +// skip-classified row calls t.Skip(reason) before asserting anything; that +// reason is what "-v" output and go test's own --- SKIP lines report. +func TestConformance_UVErrorPositions(t *testing.T) { + for _, c := range uvCases { + t.Run(c.name, func(t *testing.T) { + if c.skip != "" { + t.Skip(c.skip) + } + _, err := ParseRequirement(NewTokenizer(c.input)) + require.Error(t, err, "input %q should be rejected", c.input) + + var se *SyntaxError + require.ErrorAs(t, err, &se, "error chain should contain a *SyntaxError") + assert.Equal(t, c.wantStart, se.Start, "Start") + assert.Equal(t, c.wantEnd, se.End, "End") + assert.Equal(t, c.wantMsg, se.Msg, "message is ours, not uv's") + + lines := strings.Split(se.Error(), "\n") + require.Len(t, lines, 3, "Msg + Source + caret line") + assert.Equal(t, c.wantMsg, lines[0]) + assert.Equal(t, c.input, lines[1], "source line echoes the input verbatim") + }) + } +} diff --git a/marker/conformance_test.go b/marker/conformance_test.go new file mode 100644 index 0000000..06fabc0 --- /dev/null +++ b/marker/conformance_test.go @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// +// Portions of this file port test cases from pypa/packaging +// (https://github.com/pypa/packaging), specifically tests/test_markers.py's +// class TestMarker and TestOperatorEvaluation, used under the Apache License, +// Version 2.0 (dual-licensed Apache-2.0 OR BSD-2-Clause; see NOTICE for full +// license and copyright detail). +// Changed: translated from Python/pytest to Go table-driven tests; see the +// divergence notes below for cases where the Go expectation intentionally +// differs from upstream's literal assertion. +// +// Upstream pinned at 4eb0753dba8fcaaac8eb75463374e448f0931558 (identical at +// release 26.3: the only test_markers.py change since is a type annotation on +// TestNode.test_accepts_value, not ported - see below). +// +// Every upstream dict-valued evaluate() call is a PARTIAL environment: 18 of +// upstream's 40 evaluate( calls in this file pass a dict, and upstream fills +// every key the dict leaves out from the live interpreter (the issue's "~4" +// estimate was wrong). Every one is built here as baseEnvironment().With(dict) +// - never a struct literal, which would zero-fill the other ten fields and +// silently change the answer (see partial_environment_test.go). A bare +// `dict["extra"]` goes to the extraList argument instead, since With rejects +// "extra" as unknown (it is bound from the active-extras list, not the +// environment). A key that names neither a marker variable nor "extra" (e.g. +// `dict(a="a")` in test_new_string_rules) is irrelevant to a marker that never +// references it as an unquoted variable, and is dropped rather than passed to +// With. +// +// Upstream test functions with no counterpart here, and why: +// - TestNode: an internal AST node type this package does not expose. +// - TestDefaultEnvironment: reads the live interpreter (sys/platform/os); +// this package has no live-environment reader to test. +// - test_str_repr_eq_hash, test_different_markers_different_hashes, +// test_compare_markers_to_other_objects, test_hash_eq_for_combined_markers, +// test_str_preserves_nested_group_precedence: __str__/__repr__/__hash__/ +// __eq__ - Marker has String() (exercised by marker_test.go's own +// round-trip tests) but no Equal/Hash/GoString. +// - test_operator_rejects_non_marker, test_inplace_operators_fallback, +// test_right_hand_ops_and_typeerror, test_chaining_associativity_and_str, +// test_and_operator_str_equality, test_or_operator_str_equality: the +// operator-overload group (&, &=, __rand__, TypeError) - Marker has no +// combinator methods; a combined marker is written as one parsed string. +// - every test_pickle_* (8 of them): no Go serialization equivalent. +// - test_environment_with_no_extras, test_extras_and_dependency_groups_disallowed: +// use evaluate(context=...), a parameter Evaluate has no equivalent of. +// - test_extras_and_dependency_groups(_disallowed), +// test_missing_environment_key_raises_undefined_environment_name, +// test_set_valued_marker_on_lhs_raises_undefined_comparison, +// test_membership_value_str_normalization(_negated): all exercise the +// set-valued "extras" (plural, PEP 685) or "dependency_groups" (PEP 735) +// marker variables. Neither is a recognized Variable token in this +// package's grammar (only singular "extra", bound from Evaluate's +// extraList) - so these markers do not even parse here. +// - test_environment_assumes_empty_extra, test_environment_with_extra_none: +// redundant with test_evaluates' own "extra" rows and with +// evaluate_test.go's TestEvaluate_Extra_SetMembership. +// - test_evaluate_setuptools_legacy_markers: redundant with +// test_evaluate_pep345_markers' platform.python_implementation row and +// marker_test.go's TestParse_PythonImplementationAlias. +// - test_python_full_version_untagged_user_provided, test_python_full_version_untagged: +// the bare-trailing-"+" repair (repairPythonFullVersion) runs only inside +// EnvironmentFromTarget, not inside Evaluate given a caller-supplied +// Environment. Porting the first would surface a real gap - Evaluate does +// not repair "3.11.1+" before a version comparison - but fixing it is out +// of this issue's scope; the second additionally mocks the live +// interpreter. Candidate follow-up, not filed. +// - test_evaluate_does_not_mutate_cached_environment: no live-environment +// cache exists in Go; Environment is a plain, uncached struct. + +package marker + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- TestOperatorEvaluation --- + +// Ported from pypa/packaging tests/test_markers.py, +// TestOperatorEvaluation.test_prefers_pep440. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L104-L110 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L104-L110 +func TestConformance_PrefersPEP440(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{"python_full_version": "2.7.10"}) + require.NoError(t, err) + assert.True(t, evalStr(t, `"2.7.9" < python_full_version`, env, nil)) + + env, err = baseEnvironment().With(map[string]string{"python_full_version": "2.7.8"}) + require.NoError(t, err) + assert.False(t, evalStr(t, `"2.7.9" < python_full_version`, env, nil)) +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestOperatorEvaluation.test_new_string_rules. Every dict upstream passes +// here (dict(a="a")) names a key the marker text never references as an +// unquoted variable - both operands are quoted literals - so it is inert and +// dropped rather than passed to With. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L112-L123 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L112-L123 +func TestConformance_NewStringRules(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{"python_full_version": "c"}) + require.NoError(t, err) + assert.False(t, evalStr(t, `"b" < python_full_version`, env, nil)) + + env, err = baseEnvironment().With(map[string]string{"python_full_version": "a"}) + require.NoError(t, err) + assert.False(t, evalStr(t, `"b" < python_full_version`, env, nil)) + + assert.False(t, evalStr(t, `"b" > "a"`, baseEnvironment(), nil)) + assert.False(t, evalStr(t, `"b" < "a"`, baseEnvironment(), nil)) + assert.False(t, evalStr(t, `"b" >= "a"`, baseEnvironment(), nil)) + assert.False(t, evalStr(t, `"b" <= "a"`, baseEnvironment(), nil)) + assert.True(t, evalStr(t, `"a" <= "a"`, baseEnvironment(), nil)) +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestOperatorEvaluation.test_fails_when_undefined and +// test_arbitrary_equality_on_non_version_key_is_undefined. Upstream raises +// UndefinedComparison for `~=`/`===` against a non-version-typed key; +// Evaluate's fixed bool signature cannot propagate an error, so it returns +// false instead - the same documented divergence already pinned by +// evaluate_test.go's TestEvaluate_TildeEqualAndArbitraryEqual_NonVersionKey_ReturnsFalse +// (see evaluate.go's evalStringOp doc comment). +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L125-L137 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L125-L137 +func TestConformance_TildeEqualAndArbitraryEqual_NonVersionKey_Divergence(t *testing.T) { + tests := []string{ + `'2.7.0' ~= os_name`, + `os_name === 'posix'`, + `sys_platform === 'linux'`, + } + for _, expr := range tests { + t.Run(expr, func(t *testing.T) { + assert.False(t, evalStr(t, expr, baseEnvironment(), nil), + "upstream raises UndefinedComparison here; we return false (divergence, see evaluate.go)") + }) + } +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestOperatorEvaluation.test_allows_prerelease. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L139-L142 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L139-L142 +func TestConformance_AllowsPrerelease(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{"python_full_version": "3.11.0a5"}) + require.NoError(t, err) + assert.True(t, evalStr(t, `python_full_version > "3.6.2"`, env, nil)) +} + +// --- TestMarker.test_parses_valid / _invalid and friends --- + +// The three lists upstream's parametrize crosses (VARIABLES, OPERATORS, +// VALUES), transcribed verbatim so test_parses_valid, test_parses_pep345_valid +// and test_parses_setuptools_legacy_valid can each express their own +// cross-product as a Go loop rather than a hand-picked subset. +var ( + confVariables = []string{ + "extra", + "implementation_name", + "implementation_version", + "os_name", + "platform_machine", + "platform_release", + "platform_system", + "platform_version", + "python_full_version", + "python_version", + "platform_python_implementation", + "sys_platform", + } + confPEP345Variables = []string{ + "os.name", + "sys.platform", + "platform.version", + "platform.machine", + "platform.python_implementation", + } + confSetuptoolsVariables = []string{"python_implementation"} + confOperators = []string{"===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"} + confValues = []string{ + "1.0", + "5.6a0", + "dog", + "freebsd", + "literally any string can go here", + "things @#4 dsfd (((", + } +) + +// assertCrossProductParses asserts that Parse succeeds for every +// (variable, operator, value) triple in both operand orders, mirroring +// upstream's "{} {} {!r}".format(*i) / "{2!r} {1} {0}".format(*i) parametrize +// pair. None of confValues contains a single quote, so Python's repr() always +// single-quotes them; the Go literal does the same. Returns the case count +// (both orders) for t.Logf. +func assertCrossProductParses(t *testing.T, variables []string) int { + t.Helper() + n := 0 + for _, v := range variables { + for _, op := range confOperators { + for _, val := range confValues { + lit := "'" + val + "'" + n += 2 + if _, err := Parse(v + " " + op + " " + lit); err != nil { + t.Errorf("Parse(%q) should succeed: %v", v+" "+op+" "+lit, err) + } + if _, err := Parse(lit + " " + op + " " + v); err != nil { + t.Errorf("Parse(%q) should succeed: %v", lit+" "+op+" "+v, err) + } + } + } + } + return n +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_parses_valid. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L204-L216 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L204-L216 +func TestConformance_ParsesValid(t *testing.T) { + t.Logf("cross-product cases: %d", assertCrossProductParses(t, confVariables)) +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_parses_pep345_valid. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L428-L440 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L428-L440 +func TestConformance_ParsesPEP345Valid(t *testing.T) { + t.Logf("cross-product cases: %d", assertCrossProductParses(t, confPEP345Variables)) +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_parses_setuptools_legacy_valid. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L469-L481 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L469-L481 +func TestConformance_ParsesSetuptoolsLegacyValid(t *testing.T) { + t.Logf("cross-product cases: %d", assertCrossProductParses(t, confSetuptoolsVariables)) +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_parses_invalid. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L218-L231 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L218-L231 +func TestConformance_ParsesInvalid(t *testing.T) { + tests := []string{ + "this_isnt_a_real_variable >= '1.0'", + "python_version", + "(python_version)", + "python_version >= 1.0 and (python_version)", + `(python_version == "2.7" and os_name == "linux"`, + `(python_version == "2.7") with random text`, + } + for _, s := range tests { + t.Run(s, func(t *testing.T) { + _, err := Parse(s) + assert.Error(t, err, "input %q should be rejected", s) + }) + } +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestMarker.test_parses_invalid_trailing_line_break. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L233-L236 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L233-L236 +func TestConformance_ParsesInvalidTrailingLineBreak(t *testing.T) { + for _, lb := range []string{"\n", "\r", "\r\n"} { + t.Run(escapeForName(lb), func(t *testing.T) { + _, err := Parse(`python_version >= "3"` + lb) + assert.Error(t, err, "trailing line break should be rejected") + }) + } +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestMarker.test_parses_trailing_horizontal_whitespace. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L238-L242 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L238-L242 +func TestConformance_ParsesTrailingHorizontalWhitespace(t *testing.T) { + want, err := Parse(`python_version >= "3"`) + require.NoError(t, err) + for _, ws := range []string{" ", "\t", " \t"} { + t.Run(escapeForName(ws), func(t *testing.T) { + got, err := Parse(`python_version >= "3"` + ws) + require.NoError(t, err, "trailing horizontal whitespace is legal") + assert.Equal(t, want.String(), got.String()) + }) + } +} + +// escapeForName renders whitespace visibly so subtest names stay distinct +// (mirrors internal/pep508's own helper of the same purpose). +func escapeForName(s string) string { + out := "" + for _, r := range s { + switch r { + case '\n': + out += "LF" + case '\r': + out += "CR" + case '\t': + out += "TAB" + case ' ': + out += "SP" + default: + out += string(r) + } + } + return out +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestMarker.test_parses_invalid_malformed_quoted_string. Upstream also +// asserts an exact wrapped message and a tilde-caret rendering specific to +// Python's InvalidMarker text, which this file does not replicate - the +// byte-position-level assertions on malformed quoted strings live in +// internal/pep508/marker_test.go's #19401 escape table (this input overlaps +// it; duplication is fine, see the issue's escape-decoding note). Here we +// only confirm both upstream inputs are rejected. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L244-L267 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L244-L267 +func TestConformance_ParsesInvalidMalformedQuotedString(t *testing.T) { + tests := []string{ + `os_name == "C:\"`, + `os_name == "\x"`, + } + for _, s := range tests { + t.Run(s, func(t *testing.T) { + _, err := Parse(s) + assert.Error(t, err, "input %q should be rejected", s) + }) + } +} + +// --- test_evaluates --- + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_evaluates. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L380-L426 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L380-L426 +func TestConformance_Evaluates(t *testing.T) { + tests := []struct { + name string + expr string + overrides map[string]string + extra []string + want bool + }{ + // os.name substituted with the live os.name upstream, always true by + // construction; baseEnvironment's OsName stands in for os.name here. + {"os_name == live os.name", `os_name == "posix"`, nil, nil, true}, + {"os_name foo==foo", `os_name == "foo"`, map[string]string{"os_name": "foo"}, nil, true}, + {"os_name foo==bar", `os_name == "foo"`, map[string]string{"os_name": "bar"}, nil, false}, + {"'2.7' in python_version", `"2.7" in python_version`, map[string]string{"python_version": "2.7.5"}, nil, true}, + {"'2.7' not in python_version", `"2.7" not in python_version`, map[string]string{"python_version": "2.7"}, nil, false}, + { + "os_name and python_version ~=, both true", + `os_name == "foo" and python_version ~= "2.7.0"`, + map[string]string{"os_name": "foo", "python_version": "2.7.6"}, + nil, true, + }, + { + "~= and (or), os_name foo", + `python_version ~= "2.7.0" and (os_name == "foo" or os_name == "bar")`, + map[string]string{"os_name": "foo", "python_version": "2.7.4"}, + nil, true, + }, + { + "~= and (or), os_name bar", + `python_version ~= "2.7.0" and (os_name == "foo" or os_name == "bar")`, + map[string]string{"os_name": "bar", "python_version": "2.7.4"}, + nil, true, + }, + { + "~= and (or), os_name other", + `python_version ~= "2.7.0" and (os_name == "foo" or os_name == "bar")`, + map[string]string{"os_name": "other", "python_version": "2.7.4"}, + nil, false, + }, + // "extra" in the upstream dict goes to extraList, not With. + {"extra security, active quux", `extra == "security"`, nil, []string{"quux"}, false}, + {"extra security, active security", `extra == "security"`, nil, []string{"security"}, true}, + {"extra SECURITY literal, active security", `extra == "SECURITY"`, nil, []string{"security"}, true}, + {"extra security literal, active SECURITY", `extra == "security"`, nil, []string{"SECURITY"}, true}, + {"extra pep-685-norm literal, active raw", `extra == "pep-685-norm"`, nil, []string{"PEP_685...norm"}, true}, + { + "extra punctuation literal, active raw", + `extra == "Different.punctuation..is...equal"`, + nil, []string{"different__punctuation_is_EQUAL"}, true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env, err := baseEnvironment().With(tt.overrides) + require.NoError(t, err) + assert.Equal(t, tt.want, evalStr(t, tt.expr, env, tt.extra)) + }) + } +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_evaluate_pep345_markers. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L442-L467 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L442-L467 +func TestConformance_EvaluatePEP345Markers(t *testing.T) { + tests := []struct { + name string + expr string + overrides map[string]string + want bool + }{ + {"os.name == live os.name", `os.name == "posix"`, nil, true}, + {"sys.platform win32 vs linux2", `sys.platform == "win32"`, map[string]string{"sys_platform": "linux2"}, false}, + {"platform.version in Ubuntu", `platform.version in "Ubuntu"`, map[string]string{"platform_version": "#39"}, false}, + {"platform.machine x86_64", `platform.machine=='x86_64'`, map[string]string{"platform_machine": "x86_64"}, true}, + { + "platform.python_implementation Jython vs CPython", + `platform.python_implementation=='Jython'`, + map[string]string{"platform_python_implementation": "CPython"}, false, + }, + { + "python_version 2.5 and not Jython", + `python_version == '2.5' and platform.python_implementation!= 'Jython'`, + map[string]string{"python_version": "2.7"}, false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env, err := baseEnvironment().With(tt.overrides) + require.NoError(t, err) + assert.Equal(t, tt.want, evalStr(t, tt.expr, env, nil)) + }) + } +} + +// --- extra normalization (singular "extra" only; see file header for why +// "extras"/"dependency_groups" have no counterpart) --- + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_extra_str_normalization. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L488-L495 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L488-L495 +func TestConformance_ExtraStrNormalization(t *testing.T) { + lhs, err := Parse(`'S_P__A_M' == extra`) + require.NoError(t, err) + assert.Equal(t, `"s-p-a-m" == extra`, lhs.String()) + + rhs, err := Parse(`extra == 'S_P__A_M'`) + require.NoError(t, err) + assert.Equal(t, `extra == "s-p-a-m"`, rhs.String()) +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestMarker.test_extra_compared_to_variable_not_normalized. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L497-L501 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L497-L501 +func TestConformance_ExtraComparedToVariableNotNormalized(t *testing.T) { + m1, err := Parse("extra == os_name") + require.NoError(t, err) + assert.Equal(t, "extra == os_name", m1.String()) + + m2, err := Parse("os_name == extra") + require.NoError(t, err) + assert.Equal(t, "os_name == extra", m2.String()) +} + +// Ported from pypa/packaging tests/test_markers.py, +// TestMarker.test_nested_extra_str_normalization. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L503-L512 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L503-L512 +func TestConformance_NestedExtraStrNormalization(t *testing.T) { + m, err := Parse(`(extra == "Foo_Bar" or extra == "Baz") and python_version >= "3"`) + require.NoError(t, err) + assert.Equal(t, `(extra == "foo-bar" or extra == "baz") and python_version >= "3"`, m.String()) + + env, err := baseEnvironment().With(map[string]string{"python_version": "3.12"}) + require.NoError(t, err) + assert.True(t, m.Evaluate(env, []string{"foo-bar"})) +} + +// Ported from pypa/packaging tests/test_markers.py, TestMarker.test_version_like_equality. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L614-L633 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L614-L633 +func TestConformance_VersionLikeEquality(t *testing.T) { + tests := []struct { + name string + expr string + extra []string + want bool + }{ + {"v2, no active extra", `extra == "v2"`, nil, false}, + {"v2, active empty string", `extra == "v2"`, []string{""}, false}, + {"v2, active v2", `extra == "v2"`, []string{"v2"}, true}, + {"v2, active v2a3", `extra == "v2"`, []string{"v2a3"}, false}, + {"v2a3, active v2", `extra == "v2a3"`, []string{"v2"}, false}, + {"v2a3, active v2a3", `extra == "v2a3"`, []string{"v2a3"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, evalStr(t, tt.expr, baseEnvironment(), tt.extra)) + }) + } +} + +// --- module-level combined-marker tests --- +// +// Upstream builds these with Marker.__and__ (m1 & m2); Go's Marker has no +// combinator method (see file header), so each is expressed as one parsed +// "a and b [and c]" string - semantically identical to the combined result, +// which is all these tests exercise (str/hash equality between the two +// spellings is the operator-overload group, not ported). + +// Ported from pypa/packaging tests/test_markers.py, test_and_operator_evaluates_true. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L636-L640 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L636-L640 +func TestConformance_AndOperatorEvaluatesTrue(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{"python_version": "3.8", "os_name": "posix"}) + require.NoError(t, err) + assert.True(t, evalStr(t, `python_version >= "3.6" and os_name == "posix"`, env, nil)) +} + +// Ported from pypa/packaging tests/test_markers.py, test_or_operator_evaluates_true. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L650-L654 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L650-L654 +func TestConformance_OrOperatorEvaluatesTrue(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{"python_version": "3.7", "os_name": "windows"}) + require.NoError(t, err) + assert.True(t, evalStr(t, `python_version < "3.6" or os_name == "windows"`, env, nil)) +} + +// Ported from pypa/packaging tests/test_markers.py, test_evaluation_of_combined_markers. +// Pinned: 4eb0753dba8fcaaac8eb75463374e448f0931558 (L720-L727 at that commit). +// https://github.com/pypa/packaging/blob/4eb0753dba8fcaaac8eb75463374e448f0931558/tests/test_markers.py#L720-L727 +func TestConformance_EvaluationOfCombinedMarkers(t *testing.T) { + env, err := baseEnvironment().With(map[string]string{ + "python_version": "3.8", "os_name": "posix", "platform_system": "Linux", + }) + require.NoError(t, err) + assert.True(t, evalStr(t, + `python_version >= "3.6" and os_name == "posix" and platform_system == "Linux"`, + env, nil)) +}