diff --git a/.editorconfig b/.editorconfig index fe741e41..4216e54e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -95,6 +95,108 @@ dotnet_diagnostic.IDE0161.severity = warning # Naming fixes rename symbols, which dotnet format cannot apply safely; keep them as editor guidance. dotnet_diagnostic.IDE1006.severity = suggestion +################################################################################ +# Second hardening pass (2026-09): closes rules the .NET 10 SDK ships at +# Suggestion or Silent severity by default. EnforceCodeStyleInBuild only runs +# these analyzers at build time; it does NOT promote their severity. Only +# 'warning' (via TreatWarningsAsErrors) or 'error' fails the build, so every +# dotnet_diagnostic.IDEXXXX / csharp_style_* / dotnet_style_* line below +# carries an explicit severity for that reason - a bare option value with no +# ':severity' suffix silently keeps the SDK default, which is frequently +# Silent and therefore a no-op in CI no matter how the analyzer is configured. +################################################################################ + +# Pattern-matching preference: already 'true' by SDK default, only severity was missing. +# Roslyn only offers the fix when it is provably behavior-preserving. +dotnet_diagnostic.IDE0078.severity = warning +dotnet_diagnostic.IDE0260.severity = warning +dotnet_diagnostic.IDE0019.severity = warning +dotnet_diagnostic.IDE0020.severity = warning +dotnet_diagnostic.IDE0038.severity = warning + +# Local function over lambda when nothing is captured; avoids an implicit +# display-class/delegate allocation, which matters on the AOT src/libs surface. +dotnet_diagnostic.IDE0039.severity = warning +# Mark a capture-free local function/lambda 'static' to prevent the same allocation. +dotnet_diagnostic.IDE0062.severity = warning +dotnet_diagnostic.IDE0320.severity = warning + +# Mechanical, compiler-guaranteed-safe simplifications. +dotnet_diagnostic.IDE0042.severity = warning +dotnet_diagnostic.IDE0016.severity = warning +dotnet_diagnostic.IDE0071.severity = warning +dotnet_diagnostic.IDE0054.severity = warning +dotnet_diagnostic.IDE0074.severity = warning + +# Readonly struct / readonly member: Roslyn only offers the fix when no member +# mutates instance state. C#-level immutability only; it does not affect how +# native code writes through a pointer to the same memory, so it does not +# conflict with this SDK's blittable-struct/pointer-write interop model. +dotnet_diagnostic.IDE0250.severity = warning +dotnet_diagnostic.IDE0251.severity = warning + +# nameof(List<>) needs zero reflection metadata, unlike typeof(T).Name - the +# AOT-friendliest spelling, matching this SDK's no-reflection-type-discovery +# stance (eng/BannedSymbols.txt). C# 14+. +csharp_style_prefer_unbound_generic_type_in_nameof = true +dotnet_diagnostic.IDE0340.severity = warning + +# Prefer ArgumentNullException.ThrowIfNull / ArgumentOutOfRangeException throw +# helpers / ObjectDisposedException.ThrowIf over hand-written if-throw blocks. +# Already active at Suggestion under AnalysisLevel=10.0-recommended; Microsoft +# documents the fix as non-breaking. +dotnet_diagnostic.CA1510.severity = warning +dotnet_diagnostic.CA1511.severity = warning +dotnet_diagnostic.CA1512.severity = warning +dotnet_diagnostic.CA1513.severity = warning + +# Parameter-name consistency with the base/interface member it implements. +# The fix is a rename - the same class of fix dotnet/format#348 (open as of +# 2026-09-23) says 'dotnet format' cannot safely apply, so this stays at +# suggestion rather than joining IDE1006's exception list at a higher tier. +dotnet_diagnostic.CA1725.severity = suggestion + +# SYSLIB1054 ('use [LibraryImport], not [DllImport]') ships in-box since +# .NET 7 as part of the runtime's P/Invoke source generator; it lives outside +# AnalysisLevel/AnalysisMode entirely. No hand-written [DllImport] exists in +# this codebase today (verified), so this only guards against a future +# regression. Unlike a BannedSymbols.txt/RS0030 entry (tried, reverted), this +# compiler diagnostic correctly respects files, so it does +# not fire on LibraryImportGenerator's own generated DllImport-based fallback +# stub for non-fully-blittable signatures (libs/CheatEngine.SDK.Lua.Interop). +dotnet_diagnostic.SYSLIB1054.severity = error + +# Dead-store elimination: a value assigned to a local that is overwritten +# before being read. Safe, mechanical removal fix. +csharp_style_unused_value_assignment_preference = discard_variable:warning + +# Roslyn's fix only fires for a simple two-branch if/else; a forced ternary +# can still push past max_line_length=120 or read less explicitly than the +# block it replaces, so this stays advisory rather than build-breaking. +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion + +# Pure formatting, zero behavior change, covered by the same +# `dotnet format --verify-no-changes` CI gate as IDE0055. Removes +# operator-precedence ambiguity from mixed binary expressions. +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning + +# Flips the VALUE to match this file's own 'explicit types are the +# readability convention' stance (primary-constructor parameters become +# implicitly captured fields with different lifetime/visibility than an +# explicit field + assignment, and complicate per-parameter validation). +# Severity stays suggestion - a design judgment call, not build-breaking +# either way - mirroring IDE1006's philosophy. +csharp_style_prefer_primary_constructors = false:suggestion + +# Advisory only pending a manual pass over the Lua/ABI interop files, where a +# backing field is sometimes intentionally explicit for pointer/fixed/Unsafe +# access. +dotnet_style_prefer_auto_properties = true:suggestion + # Rider derives its own "var or explicit type" inspections from the shared # csharp_style_var_* values. Silence those copies so IDE0008 is the one # portable diagnostic developers see, rather than reporting each issue twice. @@ -108,6 +210,9 @@ resharper_suggest_var_or_type_deconstruction_declarations_highlighting = none resharper_possible_null_reference_exception_highlighting = warning # Naming rules keep public contracts recognisable before a reader opens a type. +# Evaluated top-to-bottom; the first matching rule wins, so every rule below +# that targets a modifier (async, const, static+readonly) is declared before +# the broader rule it would otherwise fall through to. dotnet_naming_rule.interfaces_must_be_prefixed_i.severity = suggestion dotnet_naming_rule.interfaces_must_be_prefixed_i.symbols = interfaces dotnet_naming_rule.interfaces_must_be_prefixed_i.style = i_pascal_case @@ -121,12 +226,55 @@ dotnet_naming_rule.types_must_be_pascal_case.style = pascal_case dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate dotnet_naming_style.pascal_case.capitalization = pascal_case +# Declared before non_private_members_must_be_pascal_case so an async method's +# suffix requirement is not shadowed by the broader rule. Any accessibility, +# so private async methods and local functions are covered too, not just +# public API. dotnet/roslyn#34833 (still open) means an interface-implementing +# or overriding async method is not always flagged - editor guidance, not a +# build gate, for that reason. +dotnet_naming_rule.async_methods_must_be_suffixed.severity = suggestion +dotnet_naming_rule.async_methods_must_be_suffixed.symbols = async_methods +dotnet_naming_rule.async_methods_must_be_suffixed.style = async_pascal_case +dotnet_naming_symbols.async_methods.applicable_kinds = method, local_function +dotnet_naming_symbols.async_methods.applicable_accessibilities = * +dotnet_naming_symbols.async_methods.required_modifiers = async +dotnet_naming_style.async_pascal_case.required_suffix = Async +dotnet_naming_style.async_pascal_case.capitalization = pascal_case + dotnet_naming_rule.non_private_members_must_be_pascal_case.severity = suggestion dotnet_naming_rule.non_private_members_must_be_pascal_case.symbols = non_private_members dotnet_naming_rule.non_private_members_must_be_pascal_case.style = pascal_case dotnet_naming_symbols.non_private_members.applicable_kinds = property, method, event dotnet_naming_symbols.non_private_members.applicable_accessibilities = public, internal, protected, protected_internal, private_protected +# Declared before private_fields_must_be_underscore_camel_case: a const or +# static-readonly field is PascalCase regardless of accessibility (BCL +# convention), which would otherwise be shadowed by the private-field rule +# below for the private subset. const and 'static, readonly' are reported as +# distinct modifier sets by the naming-rule engine, hence two rules. +dotnet_naming_rule.constants_must_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_must_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constants_must_be_pascal_case.style = pascal_case +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = suggestion +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = * +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly + +# non_private_members (above) covers properties/methods/events but not +# fields, and enum members are reported as fields too - both previously +# matched zero naming rule anywhere in this file. +dotnet_naming_rule.non_private_fields_must_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_fields_must_be_pascal_case.symbols = non_private_fields +dotnet_naming_rule.non_private_fields_must_be_pascal_case.style = pascal_case +dotnet_naming_symbols.non_private_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_fields.applicable_accessibilities = public, internal, protected, protected_internal, private_protected + dotnet_naming_rule.private_fields_must_be_underscore_camel_case.severity = suggestion dotnet_naming_rule.private_fields_must_be_underscore_camel_case.symbols = private_fields dotnet_naming_rule.private_fields_must_be_underscore_camel_case.style = underscore_camel_case @@ -145,6 +293,9 @@ dotnet_diagnostic.CS8762.severity = none [libs/**.cs] dotnet_diagnostic.IDE0005.severity = error +# IDE-visible companion to the already-enforced RS0048 (CESDK9003 MSBuild target +# plus PublicApiFileTests.cs): purely additive, same concern, fourth layer. +dotnet_public_api_analyzer.require_api_files = true [src/**.cs] dotnet_diagnostic.IDE0005.severity = error diff --git a/CheatEngine.SDK.slnx b/CheatEngine.SDK.slnx index ccca6943..316f2ab2 100644 --- a/CheatEngine.SDK.slnx +++ b/CheatEngine.SDK.slnx @@ -1,205 +1,217 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + - + - + - - - + + + - - - - - - - - - + + + + + + + + + - - - - + + + + - + - - + + - - + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - + + - - + + - + - - + + - + - - - + + + - + - + - + - - + + - - + + - + - + - - - - - - - - + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/Directory.Packages.props b/Directory.Packages.props index 94f88314..4475bf56 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,7 +40,7 @@ - + diff --git a/README.md b/README.md index 04477705..a2bacb05 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ configuration just to run a plugin. See the [live-plugin guide](tests/CheatEngine.SDK.LivePlugin/README.md#run-it-in-cheat-engine). A NativeAOT plugin DLL is not a supported profile: Cheat Engine unloads a plugin with `FreeLibrary`, which .NET does -not support for a NativeAOT library. See [load profiles and limits](src/CheatEngine.SDK/README.md#load-profiles-and-limits). +not support for a NativeAOT library. +See [load profiles and limits](src/CheatEngine.SDK/README.md#load-profiles-and-limits). ## Install @@ -120,15 +121,15 @@ C toolchain to rebuild it. ## Project layout -| Path | Purpose | -|------------------------------------------------|-----------------------------------------------------------------------------| -| [`libs/`](libs/) | Layered annotations, ABI, Lua, engine, and hosting libraries. | -| [`src/CheatEngine.SDK/`](src/CheatEngine.SDK/) | The `CheatEngine.SDK` NuGet package and consumer build properties. | -| [`source-generators/`](source-generators/) | Generated plugin entry-point and Lua-binding components. | -| [`analyzers/`](analyzers/) | Diagnostics, code fixes, and their documentation. | -| [`native/`](native/) | The Lua test fixture and bundled Windows x64 Lua protection bridge. | -| [`tests/`](tests/) | Unit tests, benchmarks, shared fixtures, and the live-plugin sample. | -| [`exemples/`](exemples/) | Guides, recipes, and API documentation. The directory name is intentional. | +| Path | Purpose | +|------------------------------------------------|---------------------------------------------------------------------------------| +| [`libs/`](libs/) | Layered annotations, ABI, Lua, engine, and hosting libraries. | +| [`src/CheatEngine.SDK/`](src/CheatEngine.SDK/) | The `CheatEngine.SDK` NuGet package and consumer build properties. | +| [`source-generators/`](source-generators/) | Generated plugin entry-point and Lua-binding components. | +| [`analyzers/`](analyzers/) | Diagnostics, code fixes, and their documentation. | +| [`native/`](native/) | The Lua test fixture and bundled Windows x64 Lua protection bridge. | +| [`tests/`](tests/) | Unit tests, benchmarks, shared fixtures, and the live-plugin sample. | +| [`exemples/`](exemples/) | Guides, recipes, and API documentation. The directory name is intentional. | | [`eng/`](eng/) | Shared MSBuild configuration (`Directory.Build.*`, central package management). | ## Documentation diff --git a/RELEASING.md b/RELEASING.md index 766d8522..89e14b14 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -22,15 +22,15 @@ verify ─► ci ─► attest ─► draft-release ─► publish ─► verify └ build and test the tag in Debug and Release, pack once, test that exact nupkg ``` -| Job | What it does | -|---|---| -| `verify` | Checks the SemVer tag, that it points to the first-parent history of `main`, that the version is not on nuget.org yet, and extracts the release notes from `CHANGELOG.md`. | -| `ci` | Runs `ci.yml` on the tag. The Release leg packs `CheatEngine.SDK..nupkg` (the file name must match the tag), runs the packaging tests on that exact file (`CESDK_PACKAGED_UMBRELLA_NUPKG`), and uploads it as `nuget-package`. | -| `attest` | Extracts the SPDX 2.2 SBOM embedded in the nupkg, creates the SLSA provenance attestation and the SBOM attestation of the nupkg (predicate `https://spdx.dev/Document/v2.2`), verifies both, and writes `SHA256SUMS`. | -| `draft-release` | Creates a **draft** release that already carries every asset, or completes an existing draft. It never uploads to a published release. | -| `publish` | Waits for approval of the `nuget` environment, checks the nupkg against `SHA256SUMS`, logs in through NuGet trusted publishing and pushes. | -| `verify-publication` | Waits until nuget.org lists the version, downloads the repository-signed copy and checks it: `dotnet nuget verify --all`, and every zip entry byte-identical to the attested package except the added `.signature.p7s`. | -| `finalize-release` | Publishes the release, then verifies what a consumer downloads (`SHA256SUMS`, `gh release verify`, `gh attestation verify`). | +| Job | What it does | +|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `verify` | Checks the SemVer tag, that it points to the first-parent history of `main`, that the version is not on nuget.org yet, and extracts the release notes from `CHANGELOG.md`. | +| `ci` | Runs `ci.yml` on the tag. The Release leg packs `CheatEngine.SDK..nupkg` (the file name must match the tag), runs the packaging tests on that exact file (`CESDK_PACKAGED_UMBRELLA_NUPKG`), and uploads it as `nuget-package`. | +| `attest` | Extracts the SPDX 2.2 SBOM embedded in the nupkg, creates the SLSA provenance attestation and the SBOM attestation of the nupkg (predicate `https://spdx.dev/Document/v2.2`), verifies both, and writes `SHA256SUMS`. | +| `draft-release` | Creates a **draft** release that already carries every asset, or completes an existing draft. It never uploads to a published release. | +| `publish` | Waits for approval of the `nuget` environment, checks the nupkg against `SHA256SUMS`, logs in through NuGet trusted publishing and pushes. | +| `verify-publication` | Waits until nuget.org lists the version, downloads the repository-signed copy and checks it: `dotnet nuget verify --all`, and every zip entry byte-identical to the attested package except the added `.signature.p7s`. | +| `finalize-release` | Publishes the release, then verifies what a consumer downloads (`SHA256SUMS`, `gh release verify`, `gh attestation verify`). | The `nuget-package` artifact is the release artifact: the file the packaging tests consumed is the file that is attested, attached to the release and pushed to nuget.org. Never upload a locally built package: nuget.org versions @@ -48,11 +48,11 @@ One release has three package identities: the attested asset, its NuGet content `verify-publication` checks all three exist and agree; none of them is written to a separate manifest. The published 1.0.0 is the worked example (verified on 2026-09-23): -| Identity | What it is | 1.0.0 | -|---|---|---| -| Attested asset SHA-256 | SHA-256 of the unsigned nupkg CI packed: the GitHub release asset and the subject of the attestations. | `99bf90101cd13e0183c94759e43badc6a1e719ffc3e2c9fd0b93490abdac0632` | -| NuGet content hash | SHA-512 (base64) of that same unsigned file. It is the `contentHash` consumer lock files store and validate on restore ([NU1403](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1403)). | `n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA==` | -| nuget.org signed file | nuget.org [repository-signs every package](https://learn.microsoft.com/nuget/reference/signed-packages-reference), so the file it serves has different bytes: the same entries plus `.signature.p7s`. | SHA-256 `3e8c98583ac71af25a5bd7053e7583fbafcd196139fae7c0b04bae9b40a7cd33`, SHA-512 `1a2B/E6reX5e636hfdb+Zdj3kT6817DuNES1RWvprhRyuyztE/56Zk2iHOMQIKpGH+O2Va8rYJxXXXTVq5aN9Q==` | +| Identity | What it is | 1.0.0 | +|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Attested asset SHA-256 | SHA-256 of the unsigned nupkg CI packed: the GitHub release asset and the subject of the attestations. | `99bf90101cd13e0183c94759e43badc6a1e719ffc3e2c9fd0b93490abdac0632` | +| NuGet content hash | SHA-512 (base64) of that same unsigned file. It is the `contentHash` consumer lock files store and validate on restore ([NU1403](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1403)). | `n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA==` | +| nuget.org signed file | nuget.org [repository-signs every package](https://learn.microsoft.com/nuget/reference/signed-packages-reference), so the file it serves has different bytes: the same entries plus `.signature.p7s`. | SHA-256 `3e8c98583ac71af25a5bd7053e7583fbafcd196139fae7c0b04bae9b40a7cd33`, SHA-512 `1a2B/E6reX5e636hfdb+Zdj3kT6817DuNES1RWvprhRyuyztE/56Zk2iHOMQIKpGH+O2Va8rYJxXXXTVq5aN9Q==` | The 1.0.0 signed copy has 28 entries and the asset 27; every common entry is byte-identical, and `dotnet nuget verify --all` on the signed copy reports the content hash above. The native bridge inside 1.0.0 @@ -107,11 +107,11 @@ releases were disabled. `CHANGELOG.md` and add its link reference. The body of that section becomes the GitHub release notes: a stable tag fails without it; a prerelease tag uses its own `## [X.Y.Z-rc.N]` section when present, otherwise `[Unreleased]`. Keep the four kinds of change the audit distinguishes apart, because each one breaks consumers differently: - - `### Added`: extensions (a new wrapper, option or API); - - `### Changed`: semantic corrections (the meaning of a result or boolean, a Lua arity or index convention, the - allowed thread, ownership conditions, an error category); - - `### Security`: refusal hardening (an operation that used to proceed and is now refused); - - `### Deployment`: package, native bridge, runtime policy or load-profile changes. + - `### Added`: extensions (a new wrapper, option or API); + - `### Changed`: semantic corrections (the meaning of a result or boolean, a Lua arity or index convention, the + allowed thread, ownership conditions, an error category); + - `### Security`: refusal hardening (an operation that used to proceed and is now refused); + - `### Deployment`: package, native bridge, runtime policy or load-profile changes. `### Removed` and `### Deprecated` are used as Keep a Changelog defines them. Add a `### Qualification waivers` section when the [qualification gate](#qualification-gate) needs one. @@ -208,8 +208,10 @@ Get-Content SHA256SUMS | ForEach-Object { - A consumer lock file holds the NuGet content hash: the `contentHash` of `CheatEngine.SDK` in `packages.lock.json` equals the base64 SHA-512 of the release asset (`[Convert]::ToBase64String([Security.Cryptography.SHA512]::HashData([IO.File]::ReadAllBytes("CheatEngine.SDK.$v.nupkg")))`). -- `dotnet nuget verify --all` on the file downloaded from nuget.org reports a nuget.org repository signature and the same - content hash ([`dotnet nuget verify`](https://learn.microsoft.com/dotnet/core/tools/dotnet-nuget-verify)). Never run it +- `dotnet nuget verify --all` on the file downloaded from nuget.org reports a nuget.org repository signature and the + same + content hash ([`dotnet nuget verify`](https://learn.microsoft.com/dotnet/core/tools/dotnet-nuget-verify)). Never run + it on the GitHub asset: that file is unsigned by design and fails with [NU3004](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu3004). - The `verify-publication` job automates these nuget.org checks directly in the workflow. diff --git a/SECURITY.md b/SECURITY.md index 287b0891..663a7057 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,11 +6,11 @@ fixes, how to report a vulnerability privately, and how to check that a package ## Supported versions -| Version | Package | Security fixes | -|---|---|---| -| 1.0.x, the latest published release line | `CheatEngine.SDK` on nuget.org | Yes, as a new 1.0.x patch release | -| 2.0.0 prereleases built from `main` | CI artifacts and prerelease packages | On `main` only; a prerelease is never patched in place | -| 0.1.0 to 0.2.1 | the former `CESDK` package ID | No; move to `CheatEngine.SDK` (see [CHANGELOG.md](CHANGELOG.md)) | +| Version | Package | Security fixes | +|------------------------------------------|--------------------------------------|------------------------------------------------------------------| +| 1.0.x, the latest published release line | `CheatEngine.SDK` on nuget.org | Yes, as a new 1.0.x patch release | +| 2.0.0 prereleases built from `main` | CI artifacts and prerelease packages | On `main` only; a prerelease is never patched in place | +| 0.1.0 to 0.2.1 | the former `CESDK` package ID | No; move to `CheatEngine.SDK` (see [CHANGELOG.md](CHANGELOG.md)) | When a new release line ships, the previous line stops receiving fixes; the table is updated in the same pull request. diff --git a/analyzers/CheatEngine.SDK.Analyzers.CodeFixes/packages.lock.json b/analyzers/CheatEngine.SDK.Analyzers.CodeFixes/packages.lock.json index e2c319fb..99041e08 100644 --- a/analyzers/CheatEngine.SDK.Analyzers.CodeFixes/packages.lock.json +++ b/analyzers/CheatEngine.SDK.Analyzers.CodeFixes/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/analyzers/CheatEngine.SDK.Analyzers/Generation/LuaGlobalProblemText.cs b/analyzers/CheatEngine.SDK.Analyzers/Generation/LuaGlobalProblemText.cs index e8ddd956..e3ddf2b4 100644 --- a/analyzers/CheatEngine.SDK.Analyzers/Generation/LuaGlobalProblemText.cs +++ b/analyzers/CheatEngine.SDK.Analyzers/Generation/LuaGlobalProblemText.cs @@ -55,7 +55,8 @@ public static DiagnosticDescriptor DescriptorFor(LuaGlobalShapeIssues problem) LuaGlobalShapeIssues.OptionalArgumentNotTrailing => DiagnosticDescriptors.NonTrailingOptionalLuaArgument, LuaGlobalShapeIssues.OptionalResultNotTrailing or LuaGlobalShapeIssues.VariadicResultNotLast or LuaGlobalShapeIssues.VariadicResultOutsideOutcome or LuaGlobalShapeIssues.UnsupportedVariadicElement - or LuaGlobalShapeIssues.MultipleVariadicResults => DiagnosticDescriptors.InvalidOptionalOrVariadicLuaResult, + or LuaGlobalShapeIssues.MultipleVariadicResults => DiagnosticDescriptors + .InvalidOptionalOrVariadicLuaResult, LuaGlobalShapeIssues.LookAlikeContractType => DiagnosticDescriptors.LookAlikeLuaContractType, LuaGlobalShapeIssues.OptionalNotSupportedHere => DiagnosticDescriptors.UnsupportedLuaOptionalPosition, _ => DiagnosticDescriptors.InvalidLuaGlobal diff --git a/analyzers/CheatEngine.SDK.Analyzers/Usage/HostWidthPointerSizeAnalyzer.cs b/analyzers/CheatEngine.SDK.Analyzers/Usage/HostWidthPointerSizeAnalyzer.cs index 7a6f9a61..f1f3b692 100644 --- a/analyzers/CheatEngine.SDK.Analyzers/Usage/HostWidthPointerSizeAnalyzer.cs +++ b/analyzers/CheatEngine.SDK.Analyzers/Usage/HostWidthPointerSizeAnalyzer.cs @@ -171,7 +171,7 @@ private static bool IsGenericSizeOf(IInvocationOperation invocation, HostWidthSy { IMethodSymbol method = invocation.TargetMethod; if (!string.Equals(method.Name, "SizeOf", StringComparison.Ordinal) || method.TypeArguments.Length != 1 - || invocation.Arguments.Length != 0) + || invocation.Arguments.Length != 0) { return false; } diff --git a/analyzers/CheatEngine.SDK.Analyzers/packages.lock.json b/analyzers/CheatEngine.SDK.Analyzers/packages.lock.json index 64d57ef4..50d815cb 100644 --- a/analyzers/CheatEngine.SDK.Analyzers/packages.lock.json +++ b/analyzers/CheatEngine.SDK.Analyzers/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/analyzers/docs/CESDK5010.md b/analyzers/docs/CESDK5010.md index 1aa1fc81..26b078c5 100644 --- a/analyzers/docs/CESDK5010.md +++ b/analyzers/docs/CESDK5010.md @@ -26,7 +26,8 @@ Your code calls one of these `CheatEngine.SDK.Engine` members, which carry Cheat Engine 7.7.0.10621 documents an optional timeout for `MemScan.waitTillDone` that returns a boolean (`celua.txt` line 2649) and a cooperative `MemScan.terminateScan` (line 2566). The C3 spike of 2026-09-22 (Lua-only, on the pinned -`ce-7.7.0.10621-x64-managed-hostfxr` profile, decision D4.7) observed `waitTillDone(timeout)` returning `true`, but could +`ce-7.7.0.10621-x64-managed-hostfxr` profile, decision D4.7) observed `waitTillDone(timeout)` returning `true`, but +could not produce the `false` path: a whole-address-space scan finished before a one-millisecond deadline expired. It did not exercise `terminateScan` at all. Everything these members do after a deadline expires is therefore covered by fixture tests (C1) only, not by the host. diff --git a/analyzers/docs/README.md b/analyzers/docs/README.md index e3c92b6f..801e7fb4 100644 --- a/analyzers/docs/README.md +++ b/analyzers/docs/README.md @@ -12,40 +12,40 @@ separate EngineApi generator owns `[Experimental]` API gates, held unqualified until their audit criteria pass. `CESDK7xxx` marks members obsoleted after 1.0.0 (`[Obsolete(DiagnosticId = …)]`). Identifiers are never renumbered or reused. -| Id | Title | Severity | Code fix | -|---------------------------|------------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------| -| [CESDK0001](CESDK0001.md) | Plugin class cannot be constructed by the generated entry point | Error | Replace `abstract` or `static` with `sealed`, add a constructor, make the constructor public | -| [CESDK0002](CESDK0002.md) | More than one plugin class in the assembly | Error | None | -| [CESDK0003](CESDK0003.md) | Manual Cheat Engine bootstrap is missing or malformed | Error | Add the exact `CESDK.CESDK.CEPluginInitialize(IntPtr, int)` contract | -| [CESDK0004](CESDK0004.md) | Plugin assembly declares a namespace under 'CESDK' | Warning | None | -| [CESDK0005](CESDK0005.md) | Source type collides with the generated Cheat Engine entry point | Error | Rename it, or explicitly own the complete manual bootstrap | -| [CESDK0006](CESDK0006.md) | Method exports a classic Cheat Engine native plugin entry point | Warning | None | -| [CESDK1001](CESDK1001.md) | Plugin startup code calls an enabled-only API | Error | Move the call to `OnEnable` | -| [CESDK1003](CESDK1003.md) | A Cheat Engine-owned value is being destroyed | Error | Keep it borrowed or use an explicit `Owned` transfer | -| [CESDK1004](CESDK1004.md) | Exception can escape an [UnmanagedCallersOnly] method | Warning | Wrap the body in try/catch | -| [CESDK1005](CESDK1005.md) | Plugin lifecycle callback must not be `async void` | Error | Keep `OnEnable`/`OnDisable` synchronous | -| [CESDK1020](CESDK1020.md) | PointerSize built from the plugin process width | Warning | None; read the target bitness or configured pointer size from Cheat Engine | -| [CESDK2001](CESDK2001.md) | Lua binding needs AllowUnsafeBlocks | Error | None | -| [CESDK2002](CESDK2002.md) | Type cannot receive a generated Lua binding part | Error | None | -| [CESDK2003](CESDK2003.md) | [LuaFunction] method cannot be exported by a generated thunk | Error | None | -| [CESDK2004](CESDK2004.md) | [LuaGlobal] method cannot receive a generated body | Error | None | -| [CESDK2005](CESDK2005.md) | Lua function name is duplicated | Error | Give one valid export a distinct Lua name | -| [CESDK2006](CESDK2006.md) | Lua annotation target cannot receive generated code | Error | Declare the supported borrowed-handle/member shape | -| [CESDK2007](CESDK2007.md) | User member collides with a generated Lua binding identity | Error | Rename the member or change the binding declaration | -| [CESDK2010](CESDK2010.md) | Optional Lua argument is not in a trailing run | Error | Declare optional arguments after the required ones | -| [CESDK2011](CESDK2011.md) | Optional or variadic Lua result shape is invalid | Error | Required, then optional results; one variadic pair last | -| [CESDK2012](CESDK2012.md) | Type impersonates an SDK Lua contract type | Error | Use the CheatEngine.SDK.Lua type | -| [CESDK2013](CESDK2013.md) | LuaOptional is not supported in this position | Error | Use an argument or an `out` result of a supported kind | -| [CESDK3001](CESDK3001.md) | Engine API specification is invalid | Error | Correct the reported spec line | -| [CESDK3002](CESDK3002.md) | Engine API specification has a generated-identity conflict | Error | Keep one spec file per generated type | -| [CESDK3003](CESDK3003.md) | Engine API specification does not declare the ce77 contract | Error | Add the `contract: ce77` header | -| [CESDK3004](CESDK3004.md) | Engine API optional argument is invalid | Error | Declare `opt:` arguments last | -| [CESDK3005](CESDK3005.md) | Engine API optional or variadic result is invalid | Error | `result:`, then `opt-result:`, then one `rest:` | -| [CESDK5001](CESDK5001.md) | Worker-thread Lua admission is experimental | Error (compiler) | None | -| [CESDK5010](CESDK5010.md) | Scan deadline and cooperative termination are experimental | Error (compiler) | None | -| [CESDK5011](CESDK5011.md) | First-found AOB scan is experimental | Error (compiler) | None | -| [CESDK7001](CESDK7001.md) | PointerSize.FromArchitecture is obsolete | Warning | None; use `TargetArchitectureObservation.ConfiguredPointerSize` or `Bitness` | -| [CESDK9102](CESDK9102.md) | A CheatEngine.SDK plugin library sets PublishAot (MSBuild, packaged build target) | Warning | None | +| Id | Title | Severity | Code fix | +|---------------------------|-----------------------------------------------------------------------------------|------------------|----------------------------------------------------------------------------------------------| +| [CESDK0001](CESDK0001.md) | Plugin class cannot be constructed by the generated entry point | Error | Replace `abstract` or `static` with `sealed`, add a constructor, make the constructor public | +| [CESDK0002](CESDK0002.md) | More than one plugin class in the assembly | Error | None | +| [CESDK0003](CESDK0003.md) | Manual Cheat Engine bootstrap is missing or malformed | Error | Add the exact `CESDK.CESDK.CEPluginInitialize(IntPtr, int)` contract | +| [CESDK0004](CESDK0004.md) | Plugin assembly declares a namespace under 'CESDK' | Warning | None | +| [CESDK0005](CESDK0005.md) | Source type collides with the generated Cheat Engine entry point | Error | Rename it, or explicitly own the complete manual bootstrap | +| [CESDK0006](CESDK0006.md) | Method exports a classic Cheat Engine native plugin entry point | Warning | None | +| [CESDK1001](CESDK1001.md) | Plugin startup code calls an enabled-only API | Error | Move the call to `OnEnable` | +| [CESDK1003](CESDK1003.md) | A Cheat Engine-owned value is being destroyed | Error | Keep it borrowed or use an explicit `Owned` transfer | +| [CESDK1004](CESDK1004.md) | Exception can escape an [UnmanagedCallersOnly] method | Warning | Wrap the body in try/catch | +| [CESDK1005](CESDK1005.md) | Plugin lifecycle callback must not be `async void` | Error | Keep `OnEnable`/`OnDisable` synchronous | +| [CESDK1020](CESDK1020.md) | PointerSize built from the plugin process width | Warning | None; read the target bitness or configured pointer size from Cheat Engine | +| [CESDK2001](CESDK2001.md) | Lua binding needs AllowUnsafeBlocks | Error | None | +| [CESDK2002](CESDK2002.md) | Type cannot receive a generated Lua binding part | Error | None | +| [CESDK2003](CESDK2003.md) | [LuaFunction] method cannot be exported by a generated thunk | Error | None | +| [CESDK2004](CESDK2004.md) | [LuaGlobal] method cannot receive a generated body | Error | None | +| [CESDK2005](CESDK2005.md) | Lua function name is duplicated | Error | Give one valid export a distinct Lua name | +| [CESDK2006](CESDK2006.md) | Lua annotation target cannot receive generated code | Error | Declare the supported borrowed-handle/member shape | +| [CESDK2007](CESDK2007.md) | User member collides with a generated Lua binding identity | Error | Rename the member or change the binding declaration | +| [CESDK2010](CESDK2010.md) | Optional Lua argument is not in a trailing run | Error | Declare optional arguments after the required ones | +| [CESDK2011](CESDK2011.md) | Optional or variadic Lua result shape is invalid | Error | Required, then optional results; one variadic pair last | +| [CESDK2012](CESDK2012.md) | Type impersonates an SDK Lua contract type | Error | Use the CheatEngine.SDK.Lua type | +| [CESDK2013](CESDK2013.md) | LuaOptional is not supported in this position | Error | Use an argument or an `out` result of a supported kind | +| [CESDK3001](CESDK3001.md) | Engine API specification is invalid | Error | Correct the reported spec line | +| [CESDK3002](CESDK3002.md) | Engine API specification has a generated-identity conflict | Error | Keep one spec file per generated type | +| [CESDK3003](CESDK3003.md) | Engine API specification does not declare the ce77 contract | Error | Add the `contract: ce77` header | +| [CESDK3004](CESDK3004.md) | Engine API optional argument is invalid | Error | Declare `opt:` arguments last | +| [CESDK3005](CESDK3005.md) | Engine API optional or variadic result is invalid | Error | `result:`, then `opt-result:`, then one `rest:` | +| [CESDK5001](CESDK5001.md) | Worker-thread Lua admission is experimental | Error (compiler) | None | +| [CESDK5010](CESDK5010.md) | Scan deadline and cooperative termination are experimental | Error (compiler) | None | +| [CESDK5011](CESDK5011.md) | First-found AOB scan is experimental | Error (compiler) | None | +| [CESDK7001](CESDK7001.md) | PointerSize.FromArchitecture is obsolete | Warning | None; use `TargetArchitectureObservation.ConfiguredPointerSize` or `Bitness` | +| [CESDK9102](CESDK9102.md) | A CheatEngine.SDK plugin library sets PublishAot (MSBuild, packaged build target) | Warning | None | Configure a rule like any other analyzer diagnostic: diff --git a/eng/Shipping.props b/eng/Shipping.props index d7ff060d..6e5277e8 100644 --- a/eng/Shipping.props +++ b/eng/Shipping.props @@ -3,10 +3,22 @@ net10.0 - + true + true + true + true + true true + + true All All diff --git a/exemples/05-aob-scans/README.md b/exemples/05-aob-scans/README.md index b1c76334..08c9de1f 100644 --- a/exemples/05-aob-scans/README.md +++ b/exemples/05-aob-scans/README.md @@ -176,7 +176,8 @@ internal static class ModuleSignatures ``` `result.IsMaterializationLimitReached` tells you the destination filled up before every row was read, and -`result.InBoundsCountIsExact` that `Written` is the full in-bounds count; a single match is proven only by that, never by +`result.InBoundsCountIsExact` that `Written` is the full in-bounds count; a single match is proven only by that, never +by a first-found scan. ```mermaid diff --git a/exemples/07-address-list/README.md b/exemples/07-address-list/README.md index 05d1f740..6070008a 100644 --- a/exemples/07-address-list/README.md +++ b/exemples/07-address-list/README.md @@ -150,7 +150,8 @@ boundary, not as an opportunity to retain a borrowed record. ## Evidence and scope -This slice is sourced from the exact CE `7.7.0.10621` x64 `celua.txt` fixture: `getAddressList`, the `Addresslist` class, and the +This slice is sourced from the exact CE `7.7.0.10621` x64 `celua.txt` fixture: `getAddressList`, the `Addresslist` +class, and the `MemoryRecord` members used above. Its protected-call tests exercise the managed boundary; they do not prove a live CE GUI thread contract. The classic plugin callback type 0 record is a separate ABI concern and is not this object API. diff --git a/libs/CheatEngine.SDK.Abi/Native/AddressListPluginInit.cs b/libs/CheatEngine.SDK.Abi/Native/AddressListPluginInit.cs index eaa9526d..da336034 100644 --- a/libs/CheatEngine.SDK.Abi/Native/AddressListPluginInit.cs +++ b/libs/CheatEngine.SDK.Abi/Native/AddressListPluginInit.cs @@ -18,7 +18,8 @@ namespace CheatEngine.SDK.Abi.Native; /// TPlugin0_SelectedRecord of the pinned plugin.pas (lines 726-735, address: ptrUint, /// ispointer: BOOL), which agrees with the C header and is mirrored by the internal /// PluginType0Record. The Pascal kit unit's two variants (address: dword, and a one-byte -/// ispointer: boolean) are known-wrong for x64 (see libs/CheatEngine.SDK.Abi/README.md). What the CE 7.7.0.10621 +/// ispointer: boolean) are known-wrong for x64 (see libs/CheatEngine.SDK.Abi/README.md). What the CE +/// 7.7.0.10621 /// binary passes is still not observed (no managed-hostfxr route registers a type-0 function), so the /// callback stays void*: no callable signature is published. /// diff --git a/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsPrefixReader.cs b/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsPrefixReader.cs index 1234f740..83a23315 100644 --- a/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsPrefixReader.cs +++ b/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsPrefixReader.cs @@ -15,7 +15,8 @@ namespace CheatEngine.SDK.Abi.Native; /// /// All or nothing. The copy succeeds only when both the declared size and the physical buffer cover the /// whole prefix, = 144 bytes, the minDeclaredSize of slot 17 in -/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json. A table that declares, say, 64 bytes yields no prefix at all, +/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json. A table that declares, +/// say, 64 bytes yields no prefix at all, /// not the first seven slots; ClassicExportedFunctionsSlotReader is the per-slot observation path. /// /// diff --git a/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsSlotReader.cs b/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsSlotReader.cs index 95c88236..84127594 100644 --- a/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsSlotReader.cs +++ b/libs/CheatEngine.SDK.Abi/Native/ClassicExportedFunctionsSlotReader.cs @@ -17,7 +17,8 @@ namespace CheatEngine.SDK.Abi.Native; /// /// /// Scope. Slots 1 to 158 of TExportedFunctions5 (plugin contract 6, 159 fields, 1272 bytes on x64, -/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json); slot 0 is the declared size itself. No managed-hostfxr plugin +/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json); slot 0 is the declared +/// size itself. No managed-hostfxr plugin /// receives this table, so there is no production caller: the classic facade is deferred and every slot stays /// NotObserved on the qualifiable profile. x64 only, like the rest of the assembly. /// diff --git a/libs/CheatEngine.SDK.Abi/Native/ClassicSlotObservation.cs b/libs/CheatEngine.SDK.Abi/Native/ClassicSlotObservation.cs index 3c56e562..676e6301 100644 --- a/libs/CheatEngine.SDK.Abi/Native/ClassicSlotObservation.cs +++ b/libs/CheatEngine.SDK.Abi/Native/ClassicSlotObservation.cs @@ -10,7 +10,9 @@ namespace CheatEngine.SDK.Abi.Native; /// /// /// An observation, not a capability: is the pointer-sized value the table held when it -/// was read. Depending on the slot (see tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json) it is a function address, +/// was read. Depending on the slot (see +/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json) it is a function +/// address, /// the address of a host cell that holds a function pointer, the address of a host variable, or nil. Nothing /// here dereferences, invokes or caches it; two reads of the same slot are two independent observations. /// diff --git a/libs/CheatEngine.SDK.Abi/Native/ExportedFunctionsPrefix.cs b/libs/CheatEngine.SDK.Abi/Native/ExportedFunctionsPrefix.cs index 2b213159..182d72b0 100644 --- a/libs/CheatEngine.SDK.Abi/Native/ExportedFunctionsPrefix.cs +++ b/libs/CheatEngine.SDK.Abi/Native/ExportedFunctionsPrefix.cs @@ -13,7 +13,8 @@ namespace CheatEngine.SDK.Abi.Native; /// validate , and impose its own lifetime, main-thread, and failure policy. /// /// -/// Evidence: slots 0-17 of the classic slot registry (tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json). Its +/// Evidence: slots 0-17 of the classic slot registry ( +/// tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json). Its /// authority is the host type TExportedFunctions5 of the pinned plugin.pas (fields lines 47-226, /// assignments of TPluginHandler.create lines 1856-2045); cepluginsdk.h and /// cepluginsdk.pas are mirror columns only. Offsets are Deduced from the declarations (x64 natural diff --git a/libs/CheatEngine.SDK.Abi/Native/NativeExportNames.cs b/libs/CheatEngine.SDK.Abi/Native/NativeExportNames.cs index aba09c29..edceb432 100644 --- a/libs/CheatEngine.SDK.Abi/Native/NativeExportNames.cs +++ b/libs/CheatEngine.SDK.Abi/Native/NativeExportNames.cs @@ -3,7 +3,8 @@ namespace CheatEngine.SDK.Abi.Native; /// /// The names of the three functions a classic native plugin DLL exports. Classic native load path only: a /// NativeAOT DLL exposing them is not a supported CheatEngine.SDK profile, because Cheat Engine unloads plugins with -/// FreeLibrary, which .NET does not support for NativeAOT libraries; see libs/CheatEngine.SDK.Abi/README.md. +/// FreeLibrary, which .NET does not support for NativeAOT libraries; see +/// libs/CheatEngine.SDK.Abi/README.md. /// /// /// diff --git a/libs/CheatEngine.SDK.Abi/Native/PluginType0Record.cs b/libs/CheatEngine.SDK.Abi/Native/PluginType0Record.cs index 9bd68569..28732c81 100644 --- a/libs/CheatEngine.SDK.Abi/Native/PluginType0Record.cs +++ b/libs/CheatEngine.SDK.Abi/Native/PluginType0Record.cs @@ -25,7 +25,8 @@ namespace CheatEngine.SDK.Abi.Native; /// of the same name with address: dword, which moves ispointer to 12 and countoffsets to /// 16 while keeping 48 bytes, and a TSelectedRecord (lines 147-156) with a one-byte /// ispointer: boolean at the host offsets. Neither is x64 authority; a size check alone cannot tell them -/// apart (per-field table in libs/CheatEngine.SDK.Abi/README.md, tests SelectedRecordOracleTests and the +/// apart (per-field table in libs/CheatEngine.SDK.Abi/README.md, tests SelectedRecordOracleTests and +/// the /// compiled fixture facts host_plugin0_selected_record, pascal_dword_mirror_selected_record, /// pascal_boolean_mirror_selected_record). /// diff --git a/libs/CheatEngine.SDK.Abi/README.md b/libs/CheatEngine.SDK.Abi/README.md index ba45a5e8..b25c5b3a 100644 --- a/libs/CheatEngine.SDK.Abi/README.md +++ b/libs/CheatEngine.SDK.Abi/README.md @@ -68,7 +68,8 @@ These installed-file hashes were recorded in a source index that was retired on remain declarations (`DeclaredRepo`) until an exact-host measurement re-measures them. The independently compiled fixture is deliberately more limited: it compiles a checked-in transcription of the pinned upstream C-header subset under MSVC x64, validates 104 facts, and compares its `sizeof`, `offsetof`, alignment, export, and topology facts -with a versioned expectation. The Debug CI test run also passes that facts file into a compiled managed test, which measures +with a versioned expectation. The Debug CI test run also passes that facts file into a compiled managed test, which +measures the matching managed record sizes, offsets, and alignments directly. It is therefore a `compiled-transcription-fixture` proof, not proof that a live CE host loads a slot, uses a given Pascal boolean width, or provides a non-null table entry. The records stay internal until a @@ -86,8 +87,8 @@ its own. Cheat Engine hands a plugin one of two exported-function tables, depending on how the plugin was loaded: -| Route | Entry point | Table the plugin receives | -|-------------------|-----------------------------------------------------|--------------------------------------------------------------------------------------------| +| Route | Entry point | Table the plugin receives | +|-------------------|----------------------------------------------------|-------------------------------------------------------------------------------------------| | Managed (hostfxr) | `CESDK.CESDK.CEPluginInitialize(IntPtr, int)` | `ManagedExportedFunctions`: 48 bytes, six fields, filled by Cheat Engine's managed loader | | Classic native | `CEPlugin_GetVersion`, `CEPlugin_InitializePlugin` | `TExportedFunctions5` of `plugin.pas`: 159 slots, 1272 bytes on x64 | @@ -109,16 +110,16 @@ The oracle for `PluginType0Record` is the host type actually passed: `TPlugin0_S with. The Pascal kit unit `cepluginsdk.pas` has two known-wrong mirrors. All three are 48 bytes, so only a per-field check separates them (`SelectedRecordOracleTests`, registry divergence D09): -| Field | Host type (oracle) | `cepluginsdk.pas` dword mirror (lines 161-170) | `cepluginsdk.pas` `TSelectedRecord` (lines 147-156) | -|-----------------------|---------------------|--------------------------------------------------|--------------------------------------------------------| -| `interpretedaddress` | 0, 8 bytes | 0, 8 bytes | 0, 8 bytes | -| `address` | 8, 8 bytes | 8, **4 bytes** | 8, 8 bytes | -| `ispointer` | 16, 4 bytes | **12**, 4 bytes | 16, **1 byte** | -| `countoffsets` | 20, 4 bytes | **16**, 4 bytes | 20, 4 bytes | -| `offsets` | 24, 8 bytes | 24, 8 bytes | 24, 8 bytes | -| `description` | 32, 8 bytes | 32, 8 bytes | 32, 8 bytes | -| `valuetype` | 40, 1 byte | 40, 1 byte | 40, 1 byte | -| `size` | 41, 1 byte | 41, 1 byte | 41, 1 byte | +| Field | Host type (oracle) | `cepluginsdk.pas` dword mirror (lines 161-170) | `cepluginsdk.pas` `TSelectedRecord` (lines 147-156) | +|----------------------|--------------------|------------------------------------------------|-----------------------------------------------------| +| `interpretedaddress` | 0, 8 bytes | 0, 8 bytes | 0, 8 bytes | +| `address` | 8, 8 bytes | 8, **4 bytes** | 8, 8 bytes | +| `ispointer` | 16, 4 bytes | **12**, 4 bytes | 16, **1 byte** | +| `countoffsets` | 20, 4 bytes | **16**, 4 bytes | 20, 4 bytes | +| `offsets` | 24, 8 bytes | 24, 8 bytes | 24, 8 bytes | +| `description` | 32, 8 bytes | 32, 8 bytes | 32, 8 bytes | +| `valuetype` | 40, 1 byte | 40, 1 byte | 40, 1 byte | +| `size` | 41, 1 byte | 41, 1 byte | 41, 1 byte | Which record the 7.7.0.10621 binary passes is `NotObserved`: no managed route reaches a type-0 registration, so the type-0 callback stays `void*`. @@ -130,13 +131,13 @@ not a supported CheatEngine.SDK profile.** Unloading a Native AOT library with ` supported by .NET (https://learn.microsoft.com/dotnet/core/deploying/native-aot/libraries, audit source EXT-01), and Cheat Engine removes a plugin with `FreeLibrary`. -| Profile | Status in CheatEngine.SDK 2.0 | Evidence and boundary | -|---|---|---| -| Historical CLR route: `MSCorEE.dll` hosting with a string entry point, as in the historical public C# template | Documentary only, **not supported** | Described by the pinned public source (`ce-public-src-ec45d5f`), which is never qualifiable. The SDK does not downgrade to that bootstrap. | -| **Managed hostfxr route** | **The only qualifiable profile**: `ce-7.7.0.10621-x64-managed-hostfxr` | Cheat Engine 7.7.0.10621 x64 (`cheatengine-x86_64.exe` SHA-256 `9727076da50924e4a097b49a02155e4b34759269c3017ff31375364b8826eb4d`), with a `ce.runtimeconfig.json` recorded as a local modification (`LocalModified`), never an installer baseline. The plugin is a framework-dependent folder; the SDK generates `int CESDK.CESDK.CEPluginInitialize(IntPtr args, int size)`, which fills the 36-byte packed `PluginInitRecord` and receives the 48-byte managed exports table. | -| NativeAOT plugin DLL | **Not supported** | Cheat Engine unloads plugins with `FreeLibrary`, which .NET does not support for NativeAOT libraries. No residence model exists in 2.0: a resident native adapter separate from an AOT component is a possible future architecture with its own contract and proof (audit A04-07, deferred), not a change of the existing profile. Q42 is recorded `NotApplicable` for this reason. | -| Classic native plugin exporting `CEPlugin_*` | **Not provided by the SDK** | The classic path receives the 159-slot classic table (the committed registry above); no managed route reaches it and the SDK has no classic facade. A package can never add native exports to a consumer: only `UnmanagedCallersOnly` methods of the published assembly become exports (https://learn.microsoft.com/dotnet/core/deploying/native-aot/interop#native-exports). | -| x86 or ARM64 host | **Not supported** | `AbiArchitecture` accepts x64 only; the packaged target refuses other explicit `PlatformTarget` values with `CESDK9101`. x64 layout tests make no x86 or ARM64 promise. | +| Profile | Status in CheatEngine.SDK 2.0 | Evidence and boundary | +|----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Historical CLR route: `MSCorEE.dll` hosting with a string entry point, as in the historical public C# template | Documentary only, **not supported** | Described by the pinned public source (`ce-public-src-ec45d5f`), which is never qualifiable. The SDK does not downgrade to that bootstrap. | +| **Managed hostfxr route** | **The only qualifiable profile**: `ce-7.7.0.10621-x64-managed-hostfxr` | Cheat Engine 7.7.0.10621 x64 (`cheatengine-x86_64.exe` SHA-256 `9727076da50924e4a097b49a02155e4b34759269c3017ff31375364b8826eb4d`), with a `ce.runtimeconfig.json` recorded as a local modification (`LocalModified`), never an installer baseline. The plugin is a framework-dependent folder; the SDK generates `int CESDK.CESDK.CEPluginInitialize(IntPtr args, int size)`, which fills the 36-byte packed `PluginInitRecord` and receives the 48-byte managed exports table. | +| NativeAOT plugin DLL | **Not supported** | Cheat Engine unloads plugins with `FreeLibrary`, which .NET does not support for NativeAOT libraries. No residence model exists in 2.0: a resident native adapter separate from an AOT component is a possible future architecture with its own contract and proof (audit A04-07, deferred), not a change of the existing profile. Q42 is recorded `NotApplicable` for this reason. | +| Classic native plugin exporting `CEPlugin_*` | **Not provided by the SDK** | The classic path receives the 159-slot classic table (the committed registry above); no managed route reaches it and the SDK has no classic facade. A package can never add native exports to a consumer: only `UnmanagedCallersOnly` methods of the published assembly become exports (https://learn.microsoft.com/dotnet/core/deploying/native-aot/interop#native-exports). | +| x86 or ARM64 host | **Not supported** | `AbiArchitecture` accepts x64 only; the packaged target refuses other explicit `PlatformTarget` values with `CESDK9101`. x64 layout tests make no x86 or ARM64 promise. | **What "AOT-compatible libraries" means.** The shipping libraries set `IsAotCompatible=true`, and `tests/CheatEngine.SDK.AotProbe` publishes the complete shipping graph as a standalone Native AOT executable. That @@ -159,10 +160,10 @@ NativeAOT publish success is never a Cheat Engine load success.** 4. **Coexistence.** Even a future residence model would have to repeat the two-plugin protocol (Q09, Q10): a solution to unloading does not isolate static state or Lua globals by itself. -| Diagnostic | Where | What it catches | -|---|---|---| -| `CESDK9102` (`analyzers/docs/CESDK9102.md`) | MSBuild warning of the packaged `build/CheatEngine.SDK.targets` (target `CheatEngineSdkWarnNativeAotPluginProfile`), direct package consumers only | A library (not `Exe`/`WinExe`) that sets `PublishAot=true`, with or without `NativeLib`. Suppressible with `NoWarn`. | -| `CESDK0006` (`analyzers/docs/CESDK0006.md`) | Roslyn analyzer shipped in the package | An `[UnmanagedCallersOnly]` method whose constant `EntryPoint` starts with `CEPlugin_`, including `NativeExportNames.*`. Unprefixed historical names are not flagged (documented limitation). | +| Diagnostic | Where | What it catches | +|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `CESDK9102` (`analyzers/docs/CESDK9102.md`) | MSBuild warning of the packaged `build/CheatEngine.SDK.targets` (target `CheatEngineSdkWarnNativeAotPluginProfile`), direct package consumers only | A library (not `Exe`/`WinExe`) that sets `PublishAot=true`, with or without `NativeLib`. Suppressible with `NoWarn`. | +| `CESDK0006` (`analyzers/docs/CESDK0006.md`) | Roslyn analyzer shipped in the package | An `[UnmanagedCallersOnly]` method whose constant `EntryPoint` starts with `CEPlugin_`, including `NativeExportNames.*`. Unprefixed historical names are not flagged (documented limitation). | **Evidence.** Q41 (NativeAOT publish and export inspection, C0/C2): C0 is `Passed` from the static contract tests below — the loader harness refuses any `CEPlugin_*` export, any missing probe export and any unexpected export diff --git a/libs/CheatEngine.SDK.Abi/packages.lock.json b/libs/CheatEngine.SDK.Abi/packages.lock.json index 75a512c2..cc7bf705 100644 --- a/libs/CheatEngine.SDK.Abi/packages.lock.json +++ b/libs/CheatEngine.SDK.Abi/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/libs/CheatEngine.SDK.Annotations/packages.lock.json b/libs/CheatEngine.SDK.Annotations/packages.lock.json index 75a512c2..cc7bf705 100644 --- a/libs/CheatEngine.SDK.Annotations/packages.lock.json +++ b/libs/CheatEngine.SDK.Annotations/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordActivationOutcome.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordActivationOutcome.cs index a85ace9a..7ff0bc40 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordActivationOutcome.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordActivationOutcome.cs @@ -9,7 +9,8 @@ namespace CheatEngine.SDK.Engine.AddressList; /// /// follows from : is /// ; and -/// are ; every +/// are ; +/// every /// other kind is . The before and after states are the values actually read; /// means that no value was observed. /// diff --git a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerOptions.cs b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerOptions.cs index f8ad8cfd..d24d7326 100644 --- a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerOptions.cs +++ b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerOptions.cs @@ -3,7 +3,8 @@ namespace CheatEngine.SDK.Engine.Assembly; /// -/// Bounds and opt-ins for +/// Bounds and opt-ins for +/// /// and . /// /// @@ -21,7 +22,8 @@ namespace CheatEngine.SDK.Engine.Assembly; /// /// /// Values are validated when the options are used: an out-of-range value makes the call throw -/// before any Lua work. Instances are immutable after initialization and +/// before any Lua work. Instances are immutable after initialization +/// and /// can be shared between threads. /// /// diff --git a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs index 9bc6a283..386981e4 100644 --- a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs +++ b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs @@ -32,7 +32,8 @@ namespace CheatEngine.SDK.Engine.Assembly; /// /// /// autoAssemble is called with three results (success, disable information or error detail, compilation -/// warnings) so that no result Cheat Engine returns is dropped. +/// warnings) so that no result Cheat Engine returns is dropped. +/// /// reports every result as an ; the older and /// keep their Boolean and exception contracts. Both globals are resolved through the SDK's /// cached, protected global push. @@ -148,7 +149,10 @@ public static AutoAssemblerApplyOutcome TryApplyWithOutcome(string script, AutoA /// Checks with autoAssembleCheck and . /// /// The Auto Assembler script to check. - /// to check the [ENABLE] section, for [DISABLE]. + /// + /// to check the [ENABLE] section, for + /// [DISABLE]. + /// /// The outcome of the check. /// is empty or white-space only. /// The plugin is not enabled or the calling thread has no Lua state. @@ -163,7 +167,10 @@ public static AutoAssemblerCheckOutcome TryCheck(string script, bool enable) /// (never targetself). /// /// The Auto Assembler script to check. - /// to check the [ENABLE] section, for [DISABLE]. + /// + /// to check the [ENABLE] section, for + /// [DISABLE]. + /// /// The host-text opt-in and bound. /// The outcome of the check. /// is empty or white-space only. @@ -338,7 +345,8 @@ internal static AutoAssemblerApplyOutcome ApplyCore(string script, AutoAssembler // Retain the original table on the stack while the copy is rooted. A failed root leaves the original table as // the one remaining authority for a direct, target-checked disable. - LuaRef? disableInfo = TryTrackDisableInfo(state, detail, warnings, disableInfoTracker, out Exception? trackingFailure); + LuaRef? disableInfo = + TryTrackDisableInfo(state, detail, warnings, disableInfoTracker, out Exception? trackingFailure); if (disableInfo is null) { cause = trackingFailure; diff --git a/libs/CheatEngine.SDK.Engine/Assembly/InstructionAssembly.cs b/libs/CheatEngine.SDK.Engine/Assembly/InstructionAssembly.cs index 1843a035..df4a1e1b 100644 --- a/libs/CheatEngine.SDK.Engine/Assembly/InstructionAssembly.cs +++ b/libs/CheatEngine.SDK.Engine/Assembly/InstructionAssembly.cs @@ -11,7 +11,9 @@ namespace CheatEngine.SDK.Engine.Assembly; /// The same instruction text assembles to different bytes for a different origin, jump preference or range-check /// option, or for another target profile. This value keeps them together with the result so that bytes are never /// reused in a context they were not produced for (audit ch.15, A15-04). It is filled on every outcome of -/// : +/// +/// : /// the context fields always echo the request, while is non-zero only on success. /// /// It owns no Lua storage and no Cheat Engine object; it stays valid after the Lua runtime detaches. diff --git a/libs/CheatEngine.SDK.Engine/Errors/EngineMarshallingException.cs b/libs/CheatEngine.SDK.Engine/Errors/EngineMarshallingException.cs index 61a54231..41a9fb49 100644 --- a/libs/CheatEngine.SDK.Engine/Errors/EngineMarshallingException.cs +++ b/libs/CheatEngine.SDK.Engine/Errors/EngineMarshallingException.cs @@ -93,7 +93,7 @@ private static string CreateDefaultMessage(string operation, EngineMarshallingDi private static EngineMarshallingDirection ValidateDirection(EngineMarshallingDirection direction) { - if (direction != EngineMarshallingDirection.Argument && direction != EngineMarshallingDirection.Result) + if (direction is not EngineMarshallingDirection.Argument and not EngineMarshallingDirection.Result) { throw new ArgumentOutOfRangeException(nameof(direction), direction, "The marshalling direction is not defined."); diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationHandoffException.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationHandoffException.cs index 03049b96..2bbef454 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationHandoffException.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationHandoffException.cs @@ -39,7 +39,8 @@ public SymbolListRegistrationReleaseOutcome CleanupOutcome private static string CreateMessage(SymbolListRegistrationReleaseOutcome cleanupOutcome) { - return "Cheat Engine registered a symbol list before its managed lease could be published; compensation ended as " + - cleanupOutcome.UnregisterKind + "."; + return + "Cheat Engine registered a symbol list before its managed lease could be published; compensation ended as " + + cleanupOutcome.UnregisterKind + "."; } } diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationLease.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationLease.cs index ddccbf9f..d3c1801c 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationLease.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationLease.cs @@ -18,7 +18,8 @@ namespace CheatEngine.SDK.Engine.Inspection; /// /// Created by , which transfers the list's into the /// lease. The order of is fixed: an already terminal lease reports -/// ; a detached runtime or another Lua universe reports +/// ; a detached runtime or another Lua universe +/// reports /// and abandons the list without any Cheat Engine call; /// an unavailable unregister member reports /// and keeps the lease retryable; an unregister() that raised reports @@ -140,7 +141,8 @@ public SymbolListRegistrationReleaseOutcome Release() if (!invoked) { // No unregister began (member absent, or its resolution failed): nothing changed, retry later. - return new SymbolListRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable, status, + return new SymbolListRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable, + status, default); } diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationReleaseOutcome.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationReleaseOutcome.cs index 4d600137..035d0bc1 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationReleaseOutcome.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolListRegistrationReleaseOutcome.cs @@ -19,7 +19,8 @@ internal SymbolListRegistrationReleaseOutcome(SymbolRegistrationReleaseKind unre /// /// Gets how the unregister step ended: , - /// , , + /// , + /// , /// (retryable) or /// . /// @@ -53,5 +54,6 @@ public TargetReleaseOutcome ListRelease /// Gets whether no later release attempt can be made through the lease. public bool IsTerminal => - UnregisterKind is not (SymbolRegistrationReleaseKind.Unknown or SymbolRegistrationReleaseKind.CleanupUnavailable); + UnregisterKind is not (SymbolRegistrationReleaseKind.Unknown + or SymbolRegistrationReleaseKind.CleanupUnavailable); } diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolLists.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolLists.cs index bcc7e6ee..3005c25c 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolLists.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolLists.cs @@ -38,7 +38,8 @@ public static class SymbolLists /// Creates one empty, unregistered, plugin-owned symbol list with createSymbolList(). /// The owner on success; otherwise. /// - /// with an owner, , + /// with an owner, + /// , /// , , or /// when Cheat Engine returned something that is not a host /// object. @@ -157,7 +158,8 @@ internal static LuaOperationStatus TryRegisterCore(Owned list, out S try { lease = leaseFactory(transferred, true) ?? - throw new InvalidOperationException("The symbol-list registration lease factory returned no lease."); + throw new InvalidOperationException( + "The symbol-list registration lease factory returned no lease."); } catch (Exception exception) { diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLease.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLease.cs index 659d02f9..2571e17b 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLease.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLease.cs @@ -17,7 +17,8 @@ namespace CheatEngine.SDK.Engine.Inspection; /// newer third-party definition is never removed. The check is best effort and not atomic: a third party can /// still replace the name between the lookup and the unregister, and a name that parses as an expression or that /// collides case-insensitively with another symbol can produce a conservative Replaced. It must never be -/// interpreted as exclusive host-wide ownership. Leases made through +/// interpreted as exclusive host-wide ownership. Leases made through +/// /// in this SDK instance additionally never unregister a newer lease's registration of the same name. /// /// diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationReleaseOutcome.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationReleaseOutcome.cs index da11ae70..f312857b 100644 --- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationReleaseOutcome.cs +++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationReleaseOutcome.cs @@ -22,9 +22,11 @@ public SymbolRegistrationReleaseKind Kind /// /// Gets the status of the last CE call the release made: the unregister call, or the name lookup for - /// , + /// , + /// /// and a lookup failure. when no CE call was made at all - /// (, , + /// (, + /// , /// before any call). This is deliberately a nullable /// value rather than (): a no-call outcome must never /// be mistaken for regardless of which member of diff --git a/libs/CheatEngine.SDK.Engine/Objects/EngineResourceOrigin.cs b/libs/CheatEngine.SDK.Engine/Objects/EngineResourceOrigin.cs index d5956eb0..6a69030a 100644 --- a/libs/CheatEngine.SDK.Engine/Objects/EngineResourceOrigin.cs +++ b/libs/CheatEngine.SDK.Engine/Objects/EngineResourceOrigin.cs @@ -64,7 +64,7 @@ public TargetProcessIncarnation? Target /// origin is never current. /// public bool IsCurrentRuntime => Runtime.AttachEpoch != 0 && LuaRuntime.IsAttached && - Runtime == LuaRuntime.CurrentStateIdentity; + Runtime == LuaRuntime.CurrentStateIdentity; /// Captures the current runtime identity, without a target component. internal static EngineResourceOrigin CaptureRuntime() diff --git a/libs/CheatEngine.SDK.Engine/Objects/Owned.cs b/libs/CheatEngine.SDK.Engine/Objects/Owned.cs index e0719fec..10458e32 100644 --- a/libs/CheatEngine.SDK.Engine/Objects/Owned.cs +++ b/libs/CheatEngine.SDK.Engine/Objects/Owned.cs @@ -57,7 +57,8 @@ namespace CheatEngine.SDK.Engine.Objects; /// Failure modes. When the destroy call raises (the object is already gone, for example destroyed by its /// parent, or its class refuses), the wrapper is still marked empty: a destroy is never retried, because the /// object may be half freed. returns that status with the message on the stack; -/// intentionally discards it after an invocation began, and +/// intentionally discards it after an invocation began, and +/// /// records it as unconfirmed. By contrast, a detached runtime, a missing pusher, or a state that is unavailable on /// the calling thread prevents an invocation from beginning. Those cases make and /// throw and retain this owner, so the plugin can retry before disable finishes or @@ -76,7 +77,6 @@ namespace CheatEngine.SDK.Engine.Objects; public sealed class Owned : IDisposable where T : struct, ICEObject { - private TargetReleaseOutcome _lastReleaseOutcome; private T _value; /// @@ -156,7 +156,11 @@ public EngineResourceOrigin Origin /// when could not begin a call. /// while the owner is live, and after a transfer or an abandonment. /// - public TargetReleaseOutcome LastReleaseOutcome => _lastReleaseOutcome; + public TargetReleaseOutcome LastReleaseOutcome + { + get; + private set; + } /// /// Destroys the object through on the ambient state, discarding a protected Lua @@ -206,7 +210,7 @@ public TargetReleaseOutcome ReleaseWithOutcome() { if (IsDisposed) { - return _lastReleaseOutcome; + return LastReleaseOutcome; } if (!IsOriginCurrent()) @@ -374,16 +378,16 @@ private bool IsOriginCurrent() private TargetReleaseOutcome Consume(LuaStatus destroyStatus) { _value = default; - _lastReleaseOutcome = destroyStatus.IsOk + LastReleaseOutcome = destroyStatus.IsOk ? TargetReleaseOutcome.Released() : TargetReleaseOutcome.Unconfirmed(EngineFailureKind.ProtectedLuaFailure); - return _lastReleaseOutcome; + return LastReleaseOutcome; } private TargetReleaseOutcome ConsumeWithoutDestroy(TargetReleaseOutcome outcome) { _value = default; - _lastReleaseOutcome = outcome; + LastReleaseOutcome = outcome; return outcome; } diff --git a/libs/CheatEngine.SDK.Engine/Processes/RuntimeHostOperations.cs b/libs/CheatEngine.SDK.Engine/Processes/RuntimeHostOperations.cs index 8af5ab5f..24bc780e 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/RuntimeHostOperations.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/RuntimeHostOperations.cs @@ -157,7 +157,8 @@ public static LuaOperationStatus TryGetTargetAbi(out TargetAbi abi) /// /// The plugin is not enabled or the calling thread has no Lua state. /// - /// getCheatEngineFileVersion may legitimately return no value; + /// getCheatEngineFileVersion may legitimately return no value; + /// /// is then , as for an absent global. Nothing is inferred between the fields. /// [RequiresPluginEnabled] diff --git a/libs/CheatEngine.SDK.Engine/Processes/RuntimeObservations.cs b/libs/CheatEngine.SDK.Engine/Processes/RuntimeObservations.cs index c95641f7..28f684e3 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/RuntimeObservations.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/RuntimeObservations.cs @@ -75,7 +75,8 @@ public static ProcessOperationStatus TryObserveRuntimeInfo(out RuntimeInfo? info try { LuaOperationStatus hostStatus = - RuntimeHostOperations.ObserveHost(state, out CheatEngineHostObservation host, out HostProbeFacts absent); + RuntimeHostOperations.ObserveHost(state, out CheatEngineHostObservation host, + out HostProbeFacts absent); if (!hostStatus.IsSuccess) { return FromHostStatus(hostStatus); @@ -150,7 +151,8 @@ private static RuntimeCapabilityAvailability Host(RuntimeCapabilityId capability // A capability is listed only when its globals were probed: unavailable when any of them is absent, available when // all of them resolved to a well-formed value. - private static void AddTarget(RuntimeCapabilityAvailability[] entries, ref int count, RuntimeCapabilityId capability, + private static void AddTarget(RuntimeCapabilityAvailability[] entries, ref int count, + RuntimeCapabilityId capability, TargetProbeFacts facts, in TargetProbeResult probe) { if ((probe.Absent & facts) != TargetProbeFacts.None) diff --git a/libs/CheatEngine.SDK.Engine/Processes/TargetArchitectureProbe.cs b/libs/CheatEngine.SDK.Engine/Processes/TargetArchitectureProbe.cs index d09e0c8a..1eaa7d1a 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/TargetArchitectureProbe.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/TargetArchitectureProbe.cs @@ -16,7 +16,8 @@ namespace CheatEngine.SDK.Engine.Processes; /// /// Every call runs inside the caller's LuaRuntimeOperation and restores the recorded stack top on every /// exit path. Each global is resolved through LuaGlobalFunctions.TryPushWithOutcome and a static -/// cache, so an absent global, a raising global (with its protected ) +/// cache, so an absent global, a raising global (with its protected +/// ) /// and a value of the wrong type stay three distinct outcomes. No error text is read. /// /// @@ -103,7 +104,10 @@ internal static TargetProbeResult Observe(LuaState state, TargetProbeFacts read, /// Reads and classifies getOpenedProcessID once, leaving the stack as it found it. /// The admitted Lua state of the caller's operation. - /// The positive identifier when the result is ; otherwise zero. + /// + /// The positive identifier when the result is ; otherwise + /// zero. + /// /// The protected status of a ; otherwise OK. /// /// , , @@ -148,7 +152,10 @@ internal static TargetProbeStatus ReadProcessId(LuaState state, out int processI /// Classifies a raw getOpenedProcessID integer. /// The Lua integer. - /// The positive identifier when the result is ; otherwise zero. + /// + /// The positive identifier when the result is ; otherwise + /// zero. + /// /// The selected, no-target, file-as-process or invalid category. internal static TargetProbeStatus ClassifyProcessId(long raw, out int processId) { @@ -255,7 +262,8 @@ private static TargetProbeStatus ReadFact(LuaState state, TargetProbeFacts read, { // CE pushes these with lua_pushinteger. A float, even an integral one, and a value outside int are // refused rather than rounded or truncated. - if (!state.IsInteger(-1) || !state.TryReadInteger(-1, out long raw) || raw is < int.MinValue or > int.MaxValue) + if (!state.IsInteger(-1) || !state.TryReadInteger(-1, out long raw) || + raw is < int.MinValue or > int.MaxValue) { return TargetProbeStatus.InvalidResult; } diff --git a/libs/CheatEngine.SDK.Engine/README.md b/libs/CheatEngine.SDK.Engine/README.md index 837a5839..973c1a5a 100644 --- a/libs/CheatEngine.SDK.Engine/README.md +++ b/libs/CheatEngine.SDK.Engine/README.md @@ -17,38 +17,38 @@ Cheat Engine. This library encodes each rule once, in a type. ## How it works -| Namespace | Types | Role | -|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `CheatEngine.SDK.Engine.Objects` | `CEObject`, `ICEObject` | Borrowed handle: the native object pointer, equal by identity, with property, index and method access | -| `CheatEngine.SDK.Engine.Objects` | `Owned`, `EngineResourceOrigin`, `EngineEffectState` | Ownership of an object the plugin created; `Dispose` destroys it once, never in another Lua universe; origin and effect vocabulary shared by every durable resource | -| `CheatEngine.SDK.Engine.Values` | `Address` | An address read from a Lua integer or hexadecimal text; its own Lua marshaller | -| `CheatEngine.SDK.Engine.Values` | `IndexBase`, `LuaSequence` | Zero-based indices over Cheat Engine objects and Lua sequences | -| `CheatEngine.SDK.Engine.Enums` | Enums, `CEEnumNames`, `EnumMarshaller` | Numeric constants, their Cheat Engine names, and Lua integer marshalling | -| `CheatEngine.SDK.Engine.Runtime` | `RuntimeInfo` (SDK-produced or caller-supplied), `RuntimeCapabilities`, `CheatEngineHostObservation`, `TargetArchitectureObservation`, `CheatEngineOperatingSystem`, `TargetBackend` | Separate host, target ISA family, bitness, configured pointer size, ABI, OS, Android and backend facts; never inferred from one another | -| `CheatEngine.SDK.Engine.Memory` | `TargetMemory`, `HostMemory`, `HostAddress` | Separate target/CE-host scalar, bounded span, target-width pointer, string and byte-table access | -| `CheatEngine.SDK.Engine.Inspection` | `EngineInspection`, `SymbolRegistry`, `SymbolLists`, `SymbolList` | Copied modules, sections, symbols, address resolution and memory-region snapshots; symbol registration leases and plugin-owned symbol lists | -| `CheatEngine.SDK.Engine.Allocation` | `TargetMemoryAllocator`, `AllocatedRegion`, `TargetAllocationAcquireOutcome` | Explicit ownership for target allocation, via a reviewed binding seam; never a live address without an owner or a reported compensation | -| `CheatEngine.SDK.Engine.Assembly` | `AutoAssemblerPatcher`, `AutoAssemblerPatch`, `InstructionProfiles`, `InstructionAssembler`, `InstructionDisassembler`, `InstructionNavigator` | Auto Assembler owns a single `disableInfo`; separately, bounded profile-qualified Lua instruction operations return copied values and structured outcomes | -| `CheatEngine.SDK.Engine.Scanning` | `AobScanner`, `AobScanBounds`, `StringList`, `MemoryScanSession` | AOB result ownership, the bounded exhaustive AOB route, and conservative MemScan/FoundList state transitions | -| `CheatEngine.SDK.Engine.AddressList` | `AddressListMutations`, `MemoryRecordId`, `MemoryRecordActivationOutcome` | ID-addressed record commands, including activation with its real effect; borrowed GUI views never become managed owners | -| `CheatEngine.SDK.Engine.Errors` | `EngineException` hierarchy, `EngineResourceHandoffException` | Stable distinction between expected CE, unavailable global, Lua, binding and marshalling failures; post-effect ownership publication reports its one cleanup attempt | -| `CheatEngine.SDK.Engine.Generated` | `MemoryScalars`, `RuntimeCapabilityProbes` | Generated wrappers: the earlier scalar memory contract, and raw read-only CE 7.7 runtime facts (ce77 spec) | +| Namespace | Types | Role | +|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `CheatEngine.SDK.Engine.Objects` | `CEObject`, `ICEObject` | Borrowed handle: the native object pointer, equal by identity, with property, index and method access | +| `CheatEngine.SDK.Engine.Objects` | `Owned`, `EngineResourceOrigin`, `EngineEffectState` | Ownership of an object the plugin created; `Dispose` destroys it once, never in another Lua universe; origin and effect vocabulary shared by every durable resource | +| `CheatEngine.SDK.Engine.Values` | `Address` | An address read from a Lua integer or hexadecimal text; its own Lua marshaller | +| `CheatEngine.SDK.Engine.Values` | `IndexBase`, `LuaSequence` | Zero-based indices over Cheat Engine objects and Lua sequences | +| `CheatEngine.SDK.Engine.Enums` | Enums, `CEEnumNames`, `EnumMarshaller` | Numeric constants, their Cheat Engine names, and Lua integer marshalling | +| `CheatEngine.SDK.Engine.Runtime` | `RuntimeInfo` (SDK-produced or caller-supplied), `RuntimeCapabilities`, `CheatEngineHostObservation`, `TargetArchitectureObservation`, `CheatEngineOperatingSystem`, `TargetBackend` | Separate host, target ISA family, bitness, configured pointer size, ABI, OS, Android and backend facts; never inferred from one another | +| `CheatEngine.SDK.Engine.Memory` | `TargetMemory`, `HostMemory`, `HostAddress` | Separate target/CE-host scalar, bounded span, target-width pointer, string and byte-table access | +| `CheatEngine.SDK.Engine.Inspection` | `EngineInspection`, `SymbolRegistry`, `SymbolLists`, `SymbolList` | Copied modules, sections, symbols, address resolution and memory-region snapshots; symbol registration leases and plugin-owned symbol lists | +| `CheatEngine.SDK.Engine.Allocation` | `TargetMemoryAllocator`, `AllocatedRegion`, `TargetAllocationAcquireOutcome` | Explicit ownership for target allocation, via a reviewed binding seam; never a live address without an owner or a reported compensation | +| `CheatEngine.SDK.Engine.Assembly` | `AutoAssemblerPatcher`, `AutoAssemblerPatch`, `InstructionProfiles`, `InstructionAssembler`, `InstructionDisassembler`, `InstructionNavigator` | Auto Assembler owns a single `disableInfo`; separately, bounded profile-qualified Lua instruction operations return copied values and structured outcomes | +| `CheatEngine.SDK.Engine.Scanning` | `AobScanner`, `AobScanBounds`, `StringList`, `MemoryScanSession` | AOB result ownership, the bounded exhaustive AOB route, and conservative MemScan/FoundList state transitions | +| `CheatEngine.SDK.Engine.AddressList` | `AddressListMutations`, `MemoryRecordId`, `MemoryRecordActivationOutcome` | ID-addressed record commands, including activation with its real effect; borrowed GUI views never become managed owners | +| `CheatEngine.SDK.Engine.Errors` | `EngineException` hierarchy, `EngineResourceHandoffException` | Stable distinction between expected CE, unavailable global, Lua, binding and marshalling failures; post-effect ownership publication reports its one cleanup attempt | +| `CheatEngine.SDK.Engine.Generated` | `MemoryScalars`, `RuntimeCapabilityProbes` | Generated wrappers: the earlier scalar memory contract, and raw read-only CE 7.7 runtime facts (ce77 spec) | The CE 7.7 vertical slices add the following public domains. They use the same protected Lua boundary, but their evidence and availability are intentionally separate: a catalogued Lua name is not a guarantee that every later CE host has the same contract. -| Namespace | Public surface | Boundary and result contract | -|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Runtime` | `RuntimeInfo`, `RuntimeCapabilities`, `RuntimeCapabilityContract`, `CheatEngineHostObservation`, `TargetArchitectureObservation`, `TargetBackend`; produced by `Processes.RuntimeObservations` | An immutable snapshot of explicitly observed version, architecture, configured pointer size, backend and availability facts. Unknown remains unknown; an available global does not fill an unobserved ownership, thread or return field, and no fact is derived from another (see [Runtime facts and target backends](#runtime-facts-and-target-backends)). | -| `Memory` | `TargetMemory`, `HostMemory`, `Address`, `HostAddress`, `PointerSize`, `MemoryAccessFailure` | Keeps attached-target addresses distinct from CE-host addresses. Target-width pointer, scalar, bounded-span, and string calls report expected CE/binding/Lua/result failures through `Try*` results; they do not claim a universal GUI-thread rule. | -| `Inspection` | `EngineInspection` and module, section, symbol and region value types | Returns copied managed snapshots. `NotFound` is used only where the CE 7.7 Lua contract documents `nil`; malformed data and Lua failures remain distinct status values. | -| `Allocation` | `TargetMemoryAllocator`, `AllocatedRegion`, `TargetAllocationAcquireOutcome` | Models one target allocation as an explicit, single-use owner bound to its Lua runtime identity and target incarnation. It does not infer a GUI-thread requirement from an unspecific CE global, and reports a failed post-effect owner handoff with its one compensation outcome. | -| `Objects` / `Scanning.Aob` | `StringList`, `StringLists`, `AobScanner`, `AobScanBounds`, `AobBoundedScanResult` | `StringLists.TryCreate` and a successful `AobScanner.TryScan` out value yield `Owned` only after a host object is returned; a failed owner publication destroys the raw list once. `TryScanWithinBounds` is the bounded, exhaustive AOB route. A list borrowed from CE must never be wrapped or destroyed by plugin code. | -| `Scanning.Values` | `MemScan`, `FoundList`, `MemoryScanSessions`, `MemoryScanSession`, scan requests and states | The factory creates and owns the scanner/child pair, retains rollback authority through publication, and the session serializes documented state transitions, stops a running scan cooperatively on release, and releases the child before the parent. It is explicitly main-thread-only; the generic `Owned` wrapper is not. | -| `AddressList` | `AddressListAccess`, `AddressList`, `MemoryRecord`, `MemoryRecordId`, `AddressListMutations` | The current GUI list and records are borrowed CE-owned handles. Mutations resolve IDs inside one protected command and report completed, not-started, or indeterminate effect (activation: before/after state, host refusal, pending); they do not promise historic record identity. | +| Namespace | Public surface | Boundary and result contract | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Runtime` | `RuntimeInfo`, `RuntimeCapabilities`, `RuntimeCapabilityContract`, `CheatEngineHostObservation`, `TargetArchitectureObservation`, `TargetBackend`; produced by `Processes.RuntimeObservations` | An immutable snapshot of explicitly observed version, architecture, configured pointer size, backend and availability facts. Unknown remains unknown; an available global does not fill an unobserved ownership, thread or return field, and no fact is derived from another (see [Runtime facts and target backends](#runtime-facts-and-target-backends)). | +| `Memory` | `TargetMemory`, `HostMemory`, `Address`, `HostAddress`, `PointerSize`, `MemoryAccessFailure` | Keeps attached-target addresses distinct from CE-host addresses. Target-width pointer, scalar, bounded-span, and string calls report expected CE/binding/Lua/result failures through `Try*` results; they do not claim a universal GUI-thread rule. | +| `Inspection` | `EngineInspection` and module, section, symbol and region value types | Returns copied managed snapshots. `NotFound` is used only where the CE 7.7 Lua contract documents `nil`; malformed data and Lua failures remain distinct status values. | +| `Allocation` | `TargetMemoryAllocator`, `AllocatedRegion`, `TargetAllocationAcquireOutcome` | Models one target allocation as an explicit, single-use owner bound to its Lua runtime identity and target incarnation. It does not infer a GUI-thread requirement from an unspecific CE global, and reports a failed post-effect owner handoff with its one compensation outcome. | +| `Objects` / `Scanning.Aob` | `StringList`, `StringLists`, `AobScanner`, `AobScanBounds`, `AobBoundedScanResult` | `StringLists.TryCreate` and a successful `AobScanner.TryScan` out value yield `Owned` only after a host object is returned; a failed owner publication destroys the raw list once. `TryScanWithinBounds` is the bounded, exhaustive AOB route. A list borrowed from CE must never be wrapped or destroyed by plugin code. | +| `Scanning.Values` | `MemScan`, `FoundList`, `MemoryScanSessions`, `MemoryScanSession`, scan requests and states | The factory creates and owns the scanner/child pair, retains rollback authority through publication, and the session serializes documented state transitions, stops a running scan cooperatively on release, and releases the child before the parent. It is explicitly main-thread-only; the generic `Owned` wrapper is not. | +| `AddressList` | `AddressListAccess`, `AddressList`, `MemoryRecord`, `MemoryRecordId`, `AddressListMutations` | The current GUI list and records are borrowed CE-owned handles. Mutations resolve IDs inside one protected command and report completed, not-started, or indeterminate effect (activation: before/after state, host refusal, pending); they do not promise historic record identity. | | `Assembly` | `InstructionTargetProfile`, `InstructionAssembler`, `InstructionAssembly`, `AssemblePreference`, `InstructionDisassembler`, `InstructionNavigator`, `InstructionDisassembly`, `InstructionOperationStatus` | `InstructionProfiles` observes PID/probe/PID under one Lua admission and maps CE's x86 family plus 64-bit flag to x64. Each instruction call validates target width and rechecks that PID before and after CE's ambient operation; its result is copied and bounded, and the detailed assembler overload echoes the origin, jump preference and range-check option it sent. That coherence check is not a target lock or a live-host qualification. | -| `Errors` | `EngineException` and stable subclasses | Separates expected operation failure, global absence, Lua failure, binding violation and marshalling violation instead of exposing a raw Lua stack error as the public Engine contract. | +| `Errors` | `EngineException` and stable subclasses | Separates expected operation failure, global absence, Lua failure, binding violation and marshalling violation instead of exposing a raw Lua stack error as the public Engine contract. | The per-capability provenance, minimum CE version, architecture, thread, ownership and return semantics are tracked against the audit's Lua surface register; executed host evidence is a separate, qualified claim. Fixture tests @@ -107,8 +107,9 @@ Engine or attaches another process. `RuntimeInfo` is an immutable snapshot, produced by `RuntimeObservations.TryObserveRuntimeInfo` or supplied by an integration layer through its legacy constructor. The SDK-produced snapshot carries a `CheatEngineHostObservation` and, -when a target is selected, a `TargetArchitectureObservation`; its `PointerSize` is Cheat Engine's configured pointer size -(`getPointerSize`), not the target bitness. It deliberately does not turn a legacy floating-point `getCEVersion` result +when a target is selected, a `TargetArchitectureObservation`; its `PointerSize` is Cheat Engine's configured pointer +size (`getPointerSize`), not the target bitness. It deliberately does not turn a legacy floating-point `getCEVersion` +result into a complete file version (it reads `getCheatEngineFileVersion` instead) and does not infer target architecture, pointer width, ABI, ownership, return semantics or thread affinity. `RuntimeCapabilities` records each probed capability as available or unavailable with the evidence fields that are actually known; a capability that was not probed stays @@ -144,7 +145,8 @@ effect, so an external selection transition in that interval, including an unsee `InstructionProfiles.TryObserveCurrent` has a narrower purpose than process qualification: it reads CE's selected PID, the documented target ISA probes, and the PID again to construct an `InstructionTargetProfile`. On Cheat Engine an x64 target is the x86 family plus the 64-bit flag (`targetIsX86() == true` and `targetIs64Bit() == true`), so the profile -follows `RuntimeInfo.TryDeriveTargetArchitecture` (see [Runtime facts and target backends](#runtime-facts-and-target-backends)): +follows `RuntimeInfo.TryDeriveTargetArchitecture` +(see [Runtime facts and target backends](#runtime-facts-and-target-backends)): x86 family gives X64 or X86, ARM family gives Arm64 or Arm32, and both families or neither is `InvalidProfile`. It never uses the managed host width or CE's configured pointer size. With no target selected CE reports x64-like facts, so a zero PID is `TargetNotSelected` before any probe; the file-as-process sentinel PID is `UnsupportedTargetBackend`. @@ -160,7 +162,8 @@ then returns managed strings; consumers never need to parse that UI text. `Instr CE's documented estimate semantics. These mappings use protected Lua globals only. The historical classic `Assembler`, `Disassembler`, `disassembleEx`, `previousOpcode`, and `nextOpcode` slots remain unprojected because their reviewed ABI and output-capacity evidence is conflicting or -insufficient; their buffer-based forms are not applicable to the managed-hostfxr profile (audit A15-19). Fixture tests verify these managed contracts; no test in this repository qualifies a live CE target, +insufficient; their buffer-based forms are not applicable to the managed-hostfxr profile (audit A15-19). Fixture tests +verify these managed contracts; no test in this repository qualifies a live CE target, target architecture, selection lock, relocation backend, or plugin Native AOT loading. `TargetMemoryAllocator` uses the production `allocateMemory`/`deAlloc` binding by default and retains @@ -179,7 +182,8 @@ allocated but no owner can be published (an unqualified target, a Lua runtime id failed owner publication), it makes exactly one target-qualified compensation and reports it as `Compensation`, so an applied allocation never leaves without an owner or a compensation result. -An allocation is bound to the runtime identity and target incarnation that created it (`AllocatedRegion.Origin`). After a +An allocation is bound to the runtime identity and target incarnation that created it (`AllocatedRegion.Origin`). After +a re-enable or a controlled state replacement the region refuses cleanup without any call (`RefusedRuntimeChanged`) and reports the residue; after a detach, cleanup cannot begin (`NotInvoked`). It never reopens or reselects a target. `Dispose` and `ReleaseWithTargetOutcome` never throw once ownership is taken; `Release` and `ReleaseWithOutcome` throw a @@ -194,7 +198,8 @@ cleanup that could not begin, and an effect requiring manual recovery. malformed-result and successful-list outcomes; `TryScan` keeps its compatible `bool` projection and supplies a caller-owned `Owned` through its `out` parameter. A valid empty list is still a successful caller-owned result, not a match classification. `AobScanner.TryScanOutcome` adds the factual distinction that a valid `StringList` -with a verified zero count is `NoMatches`; raw `nil` remains a distinct result, never a no-match inference. On the pinned +with a verified zero count is `NoMatches`; raw `nil` remains a distinct result, never a no-match inference. On the +pinned profile `ce-7.7.0.10621-x64-managed-hostfxr`, zero matches are reported this way: `AOBScan` returns no value, which the SDK reports as `NoResult`, and an empty list was never observed (host observation, spike 2026-09-22; the Q27 C3 receipt is still pending). `NoMatches` on this global route stays reserved for a valid empty list and is unreachable on that @@ -215,14 +220,16 @@ reports a factual `NoMatches` when no row lies in the bounds and CE reported no route returned exactly the in-module subset of the global route (Lua-only host observation, spike D4; the Q28 C3 receipt is still pending). A deadline overload and the session's `TryWaitForCompletion`/`TryTerminateScan` are `[Experimental("CESDK5010")]` because CE's timed-out wait and `terminateScan` were not observed on the pinned host; the -separately named first-found opt-in, `TryFindFirstFoundWithinBounds`, is `[Experimental("CESDK5011")]` because its result +separately named first-found opt-in, `TryFindFirstFoundWithinBounds`, is `[Experimental("CESDK5011")]` because its +result is "first found, order unspecified", never a uniqueness proof. See [Scan limits and cost](#scan-limits-and-cost). `MemScan` and `FoundList` are borrowed handle values, while `MemoryScanSessions.TryCreate` is the SDK's source-backed CE 7.7 creation path: it immediately owns the returned parent and child, holds one Lua operation across both calls, retains raw handles until their owner is published, rolls the child back before its parent on every later failure, and transfers the pair only to `MemoryScanSession`; an ordinary consumer cannot create an `Owned` or `Owned` -manually. Releasing a session whose scan may still run first requests one cooperative stop (`terminateScan(false)`, then a +manually. Releasing a session whose scan may still run first requests one cooperative stop (`terminateScan(false)`, then +a five-second bounded wait), then destroys the child and the parent once each even when the stop is not confirmed, which can block CE's main thread; `MemoryScanReleaseOutcome.Termination` reports the stop. CE's waits can run queued main-thread work (`waitTillDone` pumps `CheckSynchronize`) that calls back into the session: while one session member is @@ -367,19 +374,19 @@ difference as `TargetChanged`; instruction operations re-check it before and aft `TargetSelection.ObserveCurrent` and `RuntimeProcessOperations.ObserveCurrent` read it once, before their other probes, and `RuntimeProcessOperations.SelectAndObserve` reads it once, after `openProcess`, to confirm the selection. -| Fact | CE global | SDK member | When unknown | -|--------------------------------------|-----------------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------| -| CE file version | `getCheatEngineFileVersion` | `RuntimeHostOperations.TryGetCheatEngineFileVersion`, `CheatEngineHostObservation.FileVersion` | absent global or no value returned | -| CE host architecture | `getSystemArchitecture` | `RuntimeHostOperations.TryGetSystemArchitecture`, `CheatEngineHostObservation.SystemArchitecture` | absent global | -| CE is 64-bit | `cheatEngineIs64Bit` | `RuntimeHostOperations.TryIsCheatEngine64Bit`, `CheatEngineHostObservation.CheatEngineIs64Bit` | absent global; never taken from the host arch | -| CE operating system | `getOperatingSystem` | `RuntimeHostOperations.TryGetOperatingSystem`, `CheatEngineHostObservation.OperatingSystem` | absent global | -| Selected process | `getOpenedProcessID` | `TargetArchitectureObservation.ProcessId` | 0: no target, no fact is read | -| Target backend | `isConnectedToCEServer` | `TargetArchitectureObservation.Backend`, `TargetSelectionObservation.Backend` | absent global: `Unknown` | -| Target bitness (CE's 64-bit flag) | `targetIs64Bit` | `TargetArchitectureObservation.Bitness`, `CurrentProcessObservation.PointerSize` | required by the target observation | -| Target ISA family | `targetIsX86`, `targetIsArm` | `TargetArchitectureObservation.IsX86Family`, `.IsArmFamily`, `.Architecture` | absent global: `null`, architecture `Unknown` | -| Android target | `targetIsAndroid` | `TargetArchitectureObservation.IsAndroid` | absent global: `null` | -| Target ABI | `getABI` | `TargetArchitectureObservation.AbiCode`, `.Abi` | absent global, or an undocumented code (kept raw) | -| CE's configured pointer size | `getPointerSize` | `RuntimeProcessOperations.TryGetConfiguredPointerSize`, `TargetArchitectureObservation.ConfiguredPointerSize`, `RuntimeInfo.PointerSize` | absent global, or any value other than 4 or 8 (kept raw) | +| Fact | CE global | SDK member | When unknown | +|-----------------------------------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| +| CE file version | `getCheatEngineFileVersion` | `RuntimeHostOperations.TryGetCheatEngineFileVersion`, `CheatEngineHostObservation.FileVersion` | absent global or no value returned | +| CE host architecture | `getSystemArchitecture` | `RuntimeHostOperations.TryGetSystemArchitecture`, `CheatEngineHostObservation.SystemArchitecture` | absent global | +| CE is 64-bit | `cheatEngineIs64Bit` | `RuntimeHostOperations.TryIsCheatEngine64Bit`, `CheatEngineHostObservation.CheatEngineIs64Bit` | absent global; never taken from the host arch | +| CE operating system | `getOperatingSystem` | `RuntimeHostOperations.TryGetOperatingSystem`, `CheatEngineHostObservation.OperatingSystem` | absent global | +| Selected process | `getOpenedProcessID` | `TargetArchitectureObservation.ProcessId` | 0: no target, no fact is read | +| Target backend | `isConnectedToCEServer` | `TargetArchitectureObservation.Backend`, `TargetSelectionObservation.Backend` | absent global: `Unknown` | +| Target bitness (CE's 64-bit flag) | `targetIs64Bit` | `TargetArchitectureObservation.Bitness`, `CurrentProcessObservation.PointerSize` | required by the target observation | +| Target ISA family | `targetIsX86`, `targetIsArm` | `TargetArchitectureObservation.IsX86Family`, `.IsArmFamily`, `.Architecture` | absent global: `null`, architecture `Unknown` | +| Android target | `targetIsAndroid` | `TargetArchitectureObservation.IsAndroid` | absent global: `null` | +| Target ABI | `getABI` | `TargetArchitectureObservation.AbiCode`, `.Abi` | absent global, or an undocumented code (kept raw) | +| CE's configured pointer size | `getPointerSize` | `RuntimeProcessOperations.TryGetConfiguredPointerSize`, `TargetArchitectureObservation.ConfiguredPointerSize`, `RuntimeInfo.PointerSize` | absent global, or any value other than 4 or 8 (kept raw) | `RuntimeObservations.TryObserveRuntimeInfo` reads all of them in one admission and produces a `RuntimeInfo`; `RuntimeCapabilityProbes` (generated from the `ce77` runtime spec) exposes the raw values for callers that need them. @@ -409,20 +416,22 @@ is why the process identifier comes first. The configured pointer size is not the bitness. On the same host, `setPointerSize(4)` on an x64 target made `getPointerSize()` return 4 while `targetIs64Bit()` stayed true and `readPointer` kept reading 8 bytes; -`setPointerSize(2)` was accepted, and selecting the target again reset the value (spike C3 D3). The SDK therefore reports +`setPointerSize(2)` was accepted, and selecting the target again reset the value (spike C3 D3). The SDK therefore +reports both, keeps any raw configured value, never calls `setPointerSize`, and never takes a target width from `IntPtr.Size`. `PointerSize.FromArchitecture` is obsolete ([CESDK7001](../../analyzers/docs/CESDK7001.md)). The little-endian pointer -encoding of `PointerSize.TryReadLittleEndian`/`TryWriteLittleEndian` is an assumption of the local x86/x64 profile, not a +encoding of `PointerSize.TryReadLittleEndian`/`TryWriteLittleEndian` is an assumption of the local x86/x64 profile, not +a general Cheat Engine fact (audit A12-04). Target backends produce different evidence and are separate profiles (audit A12-03, ADR-11): -| Backend | How the SDK recognises it | Incarnation evidence | Qualified backend | -|-----------------|----------------------------------------------------------------------------------|-----------------------|-------------------------------------------------------| -| `LocalProcess` | `isConnectedToCEServer() == false` and a PID in (0, `int.MaxValue`] | PID + local StartTime | yes, profile `ce-7.7.0.10621-x64-managed-hostfxr` | -| `FileAsProcess` | sentinel PID 4294967295 (CE source `ec45d5f`, ObservedSource; ToQualify on 7.7) | none | no | -| `CEServer` | `isConnectedToCEServer() == true` | none | no | -| `Unknown` | `isConnectedToCEServer` absent | none | no | +| Backend | How the SDK recognises it | Incarnation evidence | Qualified backend | +|-----------------|---------------------------------------------------------------------------------|-----------------------|---------------------------------------------------| +| `LocalProcess` | `isConnectedToCEServer() == false` and a PID in (0, `int.MaxValue`] | PID + local StartTime | yes, profile `ce-7.7.0.10621-x64-managed-hostfxr` | +| `FileAsProcess` | sentinel PID 4294967295 (CE source `ec45d5f`, ObservedSource; ToQualify on 7.7) | none | no | +| `CEServer` | `isConnectedToCEServer() == true` | none | no | +| `Unknown` | `isConnectedToCEServer` absent | none | no | A file opened as a process has no operating-system process: the SDK refuses it and never searches for a Windows process (A12-07). A local BCL PID and creation time do not describe a PID served remotely by CEServer (A12-05). Cheat Engine's @@ -442,16 +451,16 @@ and never owns destroy itself. Only sourced SDK factories create owners, a gette no finalizer ever repairs a forgotten cleanup. Effects use one vocabulary, `EngineEffectState` (`Unknown = 0`, `NotStarted`, `NotApplied`, `Applied`). -| Resource | Origin captured | Release action | External-destruction risk | On target change | On Lua state change / re-enable | On detach | Retry policy | -|----------|-----------------|----------------|---------------------------|------------------|---------------------------------|-----------|--------------| -| `Owned` (StringList, SymbolList, MemScan/FoundList inside a session) | Runtime identity, inside the creating factory's admitted operation; a transfer keeps it | One `destroy()` (`Dispose`, `TryDestroy`, `ReleaseWithOutcome`) | A parent or CE destroys the object first: the destroy raises, `UnconfirmedAfterInvocation` | Not target-bound | Consumed without a call, `RefusedRuntimeChanged` (residue) | `Dispose`/`TryDestroy` throw and retain; `ReleaseWithOutcome` consumes, `NotInvoked` | Never after a destroy began | -| `AllocatedRegion` | Runtime identity and qualified incarnation, around the allocating call | One target-validated `deAlloc` | The target exits or its PID is reused: refused; a later allocation at the same address is never freed through this owner | Refused (`RefusedTargetChanged`, `RefusedProcessReused`, `RefusedNoTarget`), residue reported, never reselects | Refused without a call, `RefusedRuntimeChanged` | `NotInvoked` | Never | -| `AutoAssemblerPatch` | Runtime identity and incarnation, validated again right after the effect | One `autoAssemble(script, disableInfo)` with the rooted table | A script or table disables it, or the target exits: the disable fails, manual recovery | Refused, token kept until consumed; a change during the effect is `AppliedTargetChanged` | Refused without a call, `RefusedRuntimeChanged`; the reference is dropped without touching the new state | `NotInvoked` | Never | -| `SymbolRegistrationLease` | Runtime identity and the registered address | Name lookup, then one `unregisterSymbol(name)` only if the name still maps to the address | A third party replaces or removes the name: `Replaced` / `ExternallyRemoved`, no unregister | Not target-bound (a lookup in another target reads as a conservative `Replaced`/`ExternallyRemoved`) | `StaleRuntime`, no call | `StaleRuntime`, no call | Only `CleanupUnavailable` (no call began) | -| `SymbolListRegistrationLease` | The list owner's runtime identity | `unregister()`, then the list's one `destroy()` | CE destroys the list: `unregister()` raises, `CleanupIndeterminate`, abandoned without destroy | Not target-bound | `StaleRuntime`, list abandoned without a call | `StaleRuntime`, list abandoned without a call | Only when `unregister` is unavailable | -| `MemoryScanSession` (S-SCAN, Wave 2) | `RuntimeIdentity` and target observation (an `Origin` property is added by S-SCAN) | FoundList `deinitialize`/`destroy`, then MemScan `destroy` | CE or a parent destroys a child: unconfirmed | Refused without cleanup | Consumed without cleanup | Consumed without cleanup | Never | -| `MemoryRecord` (address list) | None: a borrowed CE-owned record | None by the plugin; `AddressListMutations` commands resolve the record by identifier in each command | A table reload, a parent delete or CE removes it: `RecordNotFound` | Not target-bound | Commands resolve again; an identifier is never a historic identity | Commands throw (lifecycle) | Never; a second delete reports `RecordNotFound` | -| Timers, hotkeys, lookup callbacks | Placeholder: S-EVT, Wave 3 | | | | | | | +| Resource | Origin captured | Release action | External-destruction risk | On target change | On Lua state change / re-enable | On detach | Retry policy | +|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|-------------------------------------------------| +| `Owned` (StringList, SymbolList, MemScan/FoundList inside a session) | Runtime identity, inside the creating factory's admitted operation; a transfer keeps it | One `destroy()` (`Dispose`, `TryDestroy`, `ReleaseWithOutcome`) | A parent or CE destroys the object first: the destroy raises, `UnconfirmedAfterInvocation` | Not target-bound | Consumed without a call, `RefusedRuntimeChanged` (residue) | `Dispose`/`TryDestroy` throw and retain; `ReleaseWithOutcome` consumes, `NotInvoked` | Never after a destroy began | +| `AllocatedRegion` | Runtime identity and qualified incarnation, around the allocating call | One target-validated `deAlloc` | The target exits or its PID is reused: refused; a later allocation at the same address is never freed through this owner | Refused (`RefusedTargetChanged`, `RefusedProcessReused`, `RefusedNoTarget`), residue reported, never reselects | Refused without a call, `RefusedRuntimeChanged` | `NotInvoked` | Never | +| `AutoAssemblerPatch` | Runtime identity and incarnation, validated again right after the effect | One `autoAssemble(script, disableInfo)` with the rooted table | A script or table disables it, or the target exits: the disable fails, manual recovery | Refused, token kept until consumed; a change during the effect is `AppliedTargetChanged` | Refused without a call, `RefusedRuntimeChanged`; the reference is dropped without touching the new state | `NotInvoked` | Never | +| `SymbolRegistrationLease` | Runtime identity and the registered address | Name lookup, then one `unregisterSymbol(name)` only if the name still maps to the address | A third party replaces or removes the name: `Replaced` / `ExternallyRemoved`, no unregister | Not target-bound (a lookup in another target reads as a conservative `Replaced`/`ExternallyRemoved`) | `StaleRuntime`, no call | `StaleRuntime`, no call | Only `CleanupUnavailable` (no call began) | +| `SymbolListRegistrationLease` | The list owner's runtime identity | `unregister()`, then the list's one `destroy()` | CE destroys the list: `unregister()` raises, `CleanupIndeterminate`, abandoned without destroy | Not target-bound | `StaleRuntime`, list abandoned without a call | `StaleRuntime`, list abandoned without a call | Only when `unregister` is unavailable | +| `MemoryScanSession` (S-SCAN, Wave 2) | `RuntimeIdentity` and target observation (an `Origin` property is added by S-SCAN) | FoundList `deinitialize`/`destroy`, then MemScan `destroy` | CE or a parent destroys a child: unconfirmed | Refused without cleanup | Consumed without cleanup | Consumed without cleanup | Never | +| `MemoryRecord` (address list) | None: a borrowed CE-owned record | None by the plugin; `AddressListMutations` commands resolve the record by identifier in each command | A table reload, a parent delete or CE removes it: `RecordNotFound` | Not target-bound | Commands resolve again; an identifier is never a historic identity | Commands throw (lifecycle) | Never; a second delete reports `RecordNotFound` | +| Timers, hotkeys, lookup callbacks | Placeholder: S-EVT, Wave 3 | | | | | | | An external `resetLuaState` that bypasses the SDK's controlled replacement is not detected today: the state generation does not advance, so this policy does not trigger; detection is a Lua runtime concern. Every row is a fixture contract @@ -472,12 +481,14 @@ A scan has four distinct limits, named the same way in the types and XML docs: The global `AOBScan` route has none of these: its native cost is a scan of the whole address space, whatever the caller filters or caps afterwards. The bounded route bounds the CE work, and its copy reads one `getAddress` per needed row and -never `getValue`. `AobBoundedScanResult` reports `HostScanElapsed` (from just before the SDK's first-scan call to the end of +never `getValue`. `AobBoundedScanResult` reports `HostScanElapsed` (from just before the SDK's first-scan call to the +end of the wait, so it also contains the SDK's two target-identity checks and `FoundList.initialize`), `CopyElapsed` (count, error text and rows) and `TotalElapsed` (session creation to release) separately. Indicative host figures, from the Lua-only C3 spike of 2026-09-22 on the pinned profile under about 41 % concurrent CPU -load (not a receipt, not SDK code): when 1 of 100 291 global matches lay in the scanned module, the range route took about +load (not a receipt, not SDK code): when 1 of 100 291 global matches lay in the scanned module, the range route took +about 31–34 ms against about 455–470 ms for the global route plus a managed filter (roughly 14 times cheaper); when all 7628 matches lay in the module, both were comparable (68–73 ms against 75–86 ms), because the per-row copy (about 5 µs per `getAddress`) dominated. This is why the copy is bounded by the destination and reads addresses only. The fixture tests @@ -517,7 +528,8 @@ The tests in `tests/CheatEngine.SDK.Engine.Tests` drive a simulated Cheat Engine 12. Inspection publishes complete snapshots only, and distinguishes `nil`, malformed result, unavailable global and Lua failure (`EngineInspectionTests`). 13. Allocation ownership is consumed once; `TryAllocate` never yields a live address without an owner or a reported - compensation, a release after detach reports `NotInvoked` and after re-enable is refused, and a released region never + compensation, a release after detach reports `NotInvoked` and after re-enable is refused, and a released region + never frees a later allocation at the same address (`AllocatedRegionTests`, `TargetMemoryAllocatorTests`, `AllocationLifecycleTests`, `TargetBoundAllocationTests`). AOB lists are owned deterministically (a failed owner publication destroys the raw list once), and the MemScan/FoundList state machine destroys its child before its diff --git a/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineHostObservation.cs b/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineHostObservation.cs index f9473272..790c7931 100644 --- a/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineHostObservation.cs +++ b/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineHostObservation.cs @@ -17,7 +17,10 @@ namespace CheatEngine.SDK.Engine.Runtime; /// /// The complete file version from getCheatEngineFileVersion, or . /// The host architecture from getSystemArchitecture, or unknown. -/// Whether cheatEngineIs64Bit reported a 64-bit Cheat Engine, or . +/// +/// Whether cheatEngineIs64Bit reported a 64-bit Cheat Engine, or +/// . +/// /// The operating system from getOperatingSystem, or unknown. [StructLayout(LayoutKind.Auto)] public readonly record struct CheatEngineHostObservation( diff --git a/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineOperatingSystem.cs b/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineOperatingSystem.cs index 9c39fbcb..869d1d56 100644 --- a/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineOperatingSystem.cs +++ b/libs/CheatEngine.SDK.Engine/Runtime/CheatEngineOperatingSystem.cs @@ -5,7 +5,8 @@ namespace CheatEngine.SDK.Engine.Runtime; /// /// Values are semantic, not the raw Lua integers: maps the /// codes documented by the CE 7.7 Lua catalogue (celua.txt:14: 0, 1, 2 for Windows, macOS and Linux). -/// The public CE source at ec45d5f returns 0 on Windows and 1 on every other build (LuaHandler.pas:14863-14867, +/// The public CE source at ec45d5f returns 0 on Windows and 1 on every other build ( +/// LuaHandler.pas:14863-14867, /// ObservedSource), so a non-Windows code is catalogue evidence only. /// /// diff --git a/libs/CheatEngine.SDK.Engine/Runtime/RuntimeInfo.cs b/libs/CheatEngine.SDK.Engine/Runtime/RuntimeInfo.cs index 7a1c7908..31325f6a 100644 --- a/libs/CheatEngine.SDK.Engine/Runtime/RuntimeInfo.cs +++ b/libs/CheatEngine.SDK.Engine/Runtime/RuntimeInfo.cs @@ -8,8 +8,10 @@ namespace CheatEngine.SDK.Engine.Runtime; /// /// /// -/// The SDK produces a snapshot with CheatEngine.SDK.Engine.Processes.RuntimeObservations.TryObserveRuntimeInfo, -/// which fills and, when a target is selected, ; the legacy properties are +/// The SDK produces a snapshot with +/// CheatEngine.SDK.Engine.Processes.RuntimeObservations.TryObserveRuntimeInfo, +/// which fills and, when a target is selected, ; the legacy properties +/// are /// then derived from those observations without inference. A snapshot can also carry caller-supplied facts /// through the legacy constructor, in which case and are /// . @@ -33,7 +35,10 @@ public sealed class RuntimeInfo /// The ABI family reported by getABI, or unknown. /// The immutable optional-capability observations for this snapshot. /// is . - /// This constructor records caller-supplied facts as-is; and stay . + /// + /// This constructor records caller-supplied facts as-is; and stay + /// . + /// public RuntimeInfo( CheatEngineVersion version, CheatEngineArchitecture systemArchitecture, @@ -76,7 +81,10 @@ public RuntimeInfo(CheatEngineHostObservation host, TargetArchitectureObservatio Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities)); } - /// Gets the Cheat Engine host observation of an SDK-produced snapshot, or for caller-supplied facts. + /// + /// Gets the Cheat Engine host observation of an SDK-produced snapshot, or for + /// caller-supplied facts. + /// public CheatEngineHostObservation? Host { get; @@ -86,7 +94,10 @@ public CheatEngineHostObservation? Host /// Gets the selected target's observation of an SDK-produced snapshot, or when no target /// was selected, its facts could not be read, or the snapshot holds caller-supplied facts. /// - /// The target bitness is ; it is not . + /// + /// The target bitness is ; it is not + /// . + /// public TargetArchitectureObservation? Target { get; diff --git a/libs/CheatEngine.SDK.Engine/Runtime/TargetArchitectureObservation.cs b/libs/CheatEngine.SDK.Engine/Runtime/TargetArchitectureObservation.cs index 9bad0e18..76d68b71 100644 --- a/libs/CheatEngine.SDK.Engine/Runtime/TargetArchitectureObservation.cs +++ b/libs/CheatEngine.SDK.Engine/Runtime/TargetArchitectureObservation.cs @@ -72,26 +72,38 @@ public TargetBackend Backend get; } - /// Gets Cheat Engine's 64-bit process flag (targetIs64Bit) as a width; this is not the configured pointer size. + /// + /// Gets Cheat Engine's 64-bit process flag (targetIs64Bit) as a width; this is not the configured pointer + /// size. + /// public PointerSize Bitness { get; } - /// Gets whether Cheat Engine reports the x86 ISA family (targetIsX86), or when absent. + /// + /// Gets whether Cheat Engine reports the x86 ISA family (targetIsX86), or when + /// absent. + /// /// An x64 target is reported as the x86 family with the 64-bit flag set. public bool? IsX86Family { get; } - /// Gets whether Cheat Engine reports the ARM ISA family (targetIsArm), or when absent. + /// + /// Gets whether Cheat Engine reports the ARM ISA family (targetIsArm), or when + /// absent. + /// public bool? IsArmFamily { get; } - /// Gets whether Cheat Engine reports an Android target (targetIsAndroid), or when absent. + /// + /// Gets whether Cheat Engine reports an Android target (targetIsAndroid), or when + /// absent. + /// /// means the global is absent; it is never read as . public bool? IsAndroid { diff --git a/libs/CheatEngine.SDK.Engine/Runtime/TargetBackend.cs b/libs/CheatEngine.SDK.Engine/Runtime/TargetBackend.cs index 188ebdd6..a40a8ce5 100644 --- a/libs/CheatEngine.SDK.Engine/Runtime/TargetBackend.cs +++ b/libs/CheatEngine.SDK.Engine/Runtime/TargetBackend.cs @@ -10,22 +10,33 @@ namespace CheatEngine.SDK.Engine.Runtime; /// /// /// -/// -/// isConnectedToCEServer() returned for a selected process identifier. +/// +/// +/// +/// +/// isConnectedToCEServer() returned for a selected process +/// identifier. +/// /// /// -/// +/// +/// +/// /// /// getOpenedProcessID() returned the file-as-process sentinel 4294967295 (CE source ec45d5f, /// ObservedSource; not yet observed on the 7.7 binary). /// /// /// -/// +/// +/// +/// /// isConnectedToCEServer() returned . /// /// -/// +/// +/// +/// /// The isConnectedToCEServer global is absent, so the backend cannot be established. /// /// diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobBoundedScan.cs b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobBoundedScan.cs index 558a4d49..74829cf4 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobBoundedScan.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobBoundedScan.cs @@ -135,7 +135,8 @@ private static AobBoundedScanOutcomeKind Execute(LuaState state, MemoryScanSessi // An omitted protection string is CE's "find everything" value, never a nil (celua.txt line 847). FirstScanRequest request = FirstScanRequest.ByteArray(pattern, bounds.Start, bounds.Stop, - options.ProtectionFlags ?? string.Empty, options.AlignmentMethod, options.AlignmentParameter ?? string.Empty); + options.ProtectionFlags ?? string.Empty, options.AlignmentMethod, + options.AlignmentParameter ?? string.Empty); long scanStarted = Stopwatch.GetTimestamp(); session.StartFirstScan(in request); AobBoundedScanOutcomeKind waited = Wait(state, session, waitMilliseconds, ref facts); @@ -228,7 +229,7 @@ private static AobBoundedScanOutcomeKind ReadRows(LuaState state, MemoryScanSess out int staging) { // Rows are addressed with CE's Int32 index; rows beyond it stay unread and make the in-bounds count inexact. - ulong readable = Math.Min(count, (ulong) int.MaxValue); + ulong readable = Math.Min(count, int.MaxValue); staging = 0; for (int index = 0; (ulong) index < readable && staging < staged.Length; index++) { diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobFirstFoundScan.cs b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobFirstFoundScan.cs index cd0b5025..e5d3873a 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobFirstFoundScan.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobFirstFoundScan.cs @@ -89,7 +89,8 @@ private static AobFirstFoundOutcomeKind Execute(LuaState state, MemoryScanSessio } FirstScanRequest request = FirstScanRequest.ByteArray(pattern, bounds.Start, bounds.Stop, - options.ProtectionFlags ?? string.Empty, options.AlignmentMethod, options.AlignmentParameter ?? string.Empty); + options.ProtectionFlags ?? string.Empty, options.AlignmentMethod, + options.AlignmentParameter ?? string.Empty); long scanStarted = Stopwatch.GetTimestamp(); session.StartFirstScan(in request); MemoryScanMaterializationStatus context = session.TryEnsureCurrentContextCore(state); @@ -152,7 +153,9 @@ private static AobFirstFoundOutcomeKind FromException(MemoryScanException except case MemoryScanFailureKind.UnexpectedResult: return AobFirstFoundOutcomeKind.InvalidResult; default: - luaStatus = exception.InnerException is LuaException lua ? FailureStatus(lua.Status) : LuaStatus.RuntimeError; + luaStatus = exception.InnerException is LuaException lua + ? FailureStatus(lua.Status) + : LuaStatus.RuntimeError; return AobFirstFoundOutcomeKind.ScanFailed; } } diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanBounds.cs b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanBounds.cs index b405d2e7..aeef3a41 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanBounds.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanBounds.cs @@ -7,13 +7,17 @@ namespace CheatEngine.SDK.Engine.Scanning.Aob; /// /// A non-empty, half-open target-address range [Start, Stop): the CE work limit of -/// . +/// +/// . /// /// /// /// is exclusive and is passed to CE as the MemScan stop address: on the pinned profile -/// ce-7.7.0.10621-x64-managed-hostfxr, CE reports a match only when it fits entirely below it (Lua-only host -/// observation, spike 2026-09-22, D4.1). is inclusive for the caller, but CE does not treat it +/// ce-7.7.0.10621-x64-managed-hostfxr, CE reports a match only when it fits entirely below it (Lua-only +/// host +/// observation, spike 2026-09-22, D4.1). is inclusive for the caller, but CE does not treat +/// it /// byte-exactly (D4.2): the bounded route drops, and counts, every address CE returns below it. /// /// @@ -65,7 +69,10 @@ public bool Contains(Address address) /// The inclusive lower bound. /// The exclusive upper bound. /// The range when the method returns ; otherwise the invalid default. - /// when the range is empty or inverted ( not above ). + /// + /// when the range is empty or inverted ( not above + /// ). + /// public static bool TryCreate(Address start, Address stop, out AobScanBounds bounds) { if (stop <= start) diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanOutcomeKind.cs b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanOutcomeKind.cs index e31f602c..28e3d923 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanOutcomeKind.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanOutcomeKind.cs @@ -14,7 +14,9 @@ namespace CheatEngine.SDK.Engine.Scanning.Aob; /// profile, is therefore unreachable on the global route, and /// means "zero matches or a host failure that also produced no list": the SDK keeps it raw rather than /// converting it to a no-match classification. Only the bounded MemScan route, -/// , +/// +/// , /// reports a factual zero on that profile. /// /// Categories come from the result's Lua type and arity only, never from a Lua error message. diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanner.cs b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanner.cs index 60f8b58b..007198d1 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanner.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Aob/AobScanner.cs @@ -325,7 +325,8 @@ public static AobBoundedScanResult TryScanWithinBounds(string pattern, AobScanBo /// the stop. The timed-out wait and terminateScan were not observed on the pinned CE 7.7.0.10621 host /// (spike D4.7), which is why this overload is experimental. /// - [Experimental("CESDK5010", UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] + [Experimental("CESDK5010", + UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] [MainThreadOnly] [RequiresPluginEnabled] public static AobBoundedScanResult TryScanWithinBounds(string pattern, AobScanBounds bounds, AobScanOptions options, @@ -342,7 +343,10 @@ public static AobBoundedScanResult TryScanWithinBounds(string pattern, AobScanBo /// /// CE's byte-array pattern text, passed without normalization. /// The CE work limit [Start, Stop); an invalid value is refused before any CE call. - /// CE's protection and alignment arguments; a protection string means "find everything". + /// + /// CE's protection and alignment arguments; a protection string means "find + /// everything". + /// /// /// Observed before the session is created and before the scan starts; it cannot interrupt the scan. /// @@ -370,7 +374,8 @@ public static AobBoundedScanResult TryScanWithinBounds(string pattern, AobScanBo /// were not observed on the pinned CE 7.7.0.10621 host, which is why it is experimental (CESDK5011). /// /// - [Experimental("CESDK5011", UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] + [Experimental("CESDK5011", + UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] [MainThreadOnly] [RequiresPluginEnabled] public static AobFirstFoundResult TryFindFirstFoundWithinBounds(string pattern, AobScanBounds bounds, diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/FirstScanRequest.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/FirstScanRequest.cs index a545b809..d1655cc4 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Values/FirstScanRequest.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/FirstScanRequest.cs @@ -216,8 +216,12 @@ public static FirstScanRequest ExactValue(VariableType variableType, string inpu /// The lower bound; not byte-exact on the pinned CE 7.7 profile (post-filter results). /// The exclusive upper bound; a match is reported only when it fits entirely below it. /// - /// The fourteen CE positions (soExactValue, vtByteArray, rtRounded, pattern, "", startAddress, stopAddress, "", - /// fsmNotAligned, "", true, false, false, false). + /// The fourteen CE positions + /// + /// (soExactValue, vtByteArray, rtRounded, pattern, "", startAddress, stopAddress, "", + /// fsmNotAligned, "", true, false, false, false) + /// + /// . /// /// is . /// @@ -245,8 +249,12 @@ public static FirstScanRequest ByteArray(string pattern, Address startAddress, A /// The address-alignment rule. /// The alignment rule's CE string parameter; empty for no alignment. /// - /// The fourteen CE positions (soExactValue, vtByteArray, rtRounded, pattern, "", startAddress, stopAddress, - /// protectionFlags, fastScanMethod, alignmentParameter, true, false, false, false). + /// The fourteen CE positions + /// + /// (soExactValue, vtByteArray, rtRounded, pattern, "", startAddress, stopAddress, + /// protectionFlags, fastScanMethod, alignmentParameter, true, false, false, false) + /// + /// . /// /// A string argument is . /// diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs index 2ea4164d..33e55081 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs @@ -85,27 +85,36 @@ public sealed class MemoryScanSession : IDisposable private const string ResultCountOperation = "MemoryScan.ResultCount"; private const string ResultAddressOperation = "MemoryScan.ResultAddress"; private const string ResultValueOperation = "MemoryScan.ResultValue"; + + /// The maximum number of UTF-8 bytes that copies from CE. + public const int HostErrorTextMaximumUtf8Bytes = 1024; + + /// + /// The bounded wait, in milliseconds, after the one cooperative stop request that release and the bounded AOB + /// route issue for a scan that may still be running. + /// + internal const int ReleaseTerminationWaitMilliseconds = 5000; + + // The member whose CE calls are in progress, or null, and whether that member is the release itself. CE's waits, + // resets and destroys can run queued main-thread work that calls back into this session (see the type remarks). + // While a member is active, every other member is refused, and a release or abandon requested meanwhile is + // recorded here and run by the active member once its CE calls have returned. + private string? _activeOperation; + private DeferredDisposal _deferredDisposal; private Owned? _foundList; private bool _isBound; - private Owned? _scanner; + private bool _releaseInProgress; // True from immediately before a firstScan/nextScan call (CE may have started work even if the call then fails) // until a wait reports completion, a cooperative stop is confirmed, or a reset succeeds. private bool _scanMayBeRunning; + private Owned? _scanner; + private TargetSelectionObservation _targetObservation; + private MemoryScanTerminationStatus _termination; // The one cooperative stop request of the current scan, if any: it is never repeated, and its status is what a // later release reports when the stop stayed unconfirmed. private bool _terminationAttempted; - private MemoryScanTerminationStatus _termination; - private TargetSelectionObservation _targetObservation; - - // The member whose CE calls are in progress, or null, and whether that member is the release itself. CE's waits, - // resets and destroys can run queued main-thread work that calls back into this session (see the type remarks). - // While a member is active, every other member is refused, and a release or abandon requested meanwhile is - // recorded here and run by the active member once its CE calls have returned. - private string? _activeOperation; - private bool _releaseInProgress; - private DeferredDisposal _deferredDisposal; private MemoryScanSession(Owned scanner, Owned foundList) { @@ -114,15 +123,6 @@ private MemoryScanSession(Owned scanner, Owned foundList) State = MemoryScanState.New; } - /// The maximum number of UTF-8 bytes that copies from CE. - public const int HostErrorTextMaximumUtf8Bytes = 1024; - - /// - /// The bounded wait, in milliseconds, after the one cooperative stop request that release and the bounded AOB - /// route issue for a scan that may still be running. - /// - internal const int ReleaseTerminationWaitMilliseconds = 5000; - /// Gets the session's conservative, managed state. public MemoryScanState State { @@ -268,6 +268,10 @@ public ulong ResultCount } } + // Whether a release or abandon was requested from inside the active member's CE calls: that member then makes no + // further CE call. + private bool IsDisposalRequested => _deferredDisposal != DeferredDisposal.None; + /// Best-effort, no-throw disposal that consumes both owners and never implicitly retries CE cleanup. /// /// Use when the factual child and parent outcomes matter. This method is @@ -726,7 +730,8 @@ public void ResetCancellable(CancellationToken cancellationToken) /// (timed-out) path was not observed on the pinned CE 7.7.0.10621 host (spike D4.7), which is why this member is /// experimental. It blocks CE's main thread for at most the deadline. /// - [Experimental("CESDK5010", UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] + [Experimental("CESDK5010", + UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] [MainThreadOnly] [RequiresPluginEnabled] public MemoryScanWaitStatus TryWaitForCompletion(TimeSpan timeout) @@ -792,7 +797,8 @@ public MemoryScanWaitStatus TryWaitForCompletion(TimeSpan timeout) /// (spike D4.7), which is why this member is experimental. /// /// - [Experimental("CESDK5010", UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] + [Experimental("CESDK5010", + UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md")] [MainThreadOnly] [RequiresPluginEnabled] public MemoryScanTerminationStatus TryTerminateScan(TimeSpan waitTimeout) @@ -1251,7 +1257,8 @@ internal static int ToWaitMilliseconds(TimeSpan timeout, string parameterName) } long ticks = timeout.Ticks; - long milliseconds = (ticks / TimeSpan.TicksPerMillisecond) + (ticks % TimeSpan.TicksPerMillisecond == 0 ? 0 : 1); + long milliseconds = (ticks / TimeSpan.TicksPerMillisecond) + + (ticks % TimeSpan.TicksPerMillisecond == 0 ? 0 : 1); if (milliseconds > int.MaxValue) { throw new ArgumentOutOfRangeException(parameterName, timeout, @@ -1620,10 +1627,6 @@ private void EndSessionCall() _ = ReleaseAsActiveCall(); } - // Whether a release or abandon was requested from inside the active member's CE calls: that member then makes no - // further CE call. - private bool IsDisposalRequested => _deferredDisposal != DeferredDisposal.None; - // After a lifecycle member returned: a release deferred from inside its CE calls has disposed the session, so the // member must not report a completed transition. private void ThrowIfReleasedDuringCall(string operation) diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanTerminationStatus.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanTerminationStatus.cs index 40e2f7fe..9b24933f 100644 --- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanTerminationStatus.cs +++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanTerminationStatus.cs @@ -1,7 +1,8 @@ namespace CheatEngine.SDK.Engine.Scanning.Values; /// -/// Classifies the one cooperative stop request for a memory scan that may still be running: terminateScan(false) +/// Classifies the one cooperative stop request for a memory scan that may still be running: +/// terminateScan(false) /// followed by a bounded waitTillDone(timeout). /// /// diff --git a/libs/CheatEngine.SDK.Engine/Tables/CheatTableFiles.cs b/libs/CheatEngine.SDK.Engine/Tables/CheatTableFiles.cs index d53c39ba..54aeba0e 100644 --- a/libs/CheatEngine.SDK.Engine/Tables/CheatTableFiles.cs +++ b/libs/CheatEngine.SDK.Engine/Tables/CheatTableFiles.cs @@ -20,7 +20,8 @@ namespace CheatEngine.SDK.Engine.Tables; /// /// Overload policy. Only the file overloads are projected. The file overload of loadTable has no /// option to suppress the Lua-script dialog, so a table that contains Lua scripts may prompt the user or execute -/// Lua. The stream overloads (loadTable(stream, merge, ignoreluascriptdialog), saveTable(stream, ...)) +/// Lua. The stream overloads (loadTable(stream, merge, ignoreluascriptdialog), +/// saveTable(stream, ...)) /// and the protect / dontDeactivateDesignerForms options of saveTable are not projected until /// a Cheat Engine stream projection exists; the Lua surface catalogue records them as deferred. /// diff --git a/libs/CheatEngine.SDK.Engine/Targets/TargetSelection.cs b/libs/CheatEngine.SDK.Engine/Targets/TargetSelection.cs index 6cadc03d..dadf4d1a 100644 --- a/libs/CheatEngine.SDK.Engine/Targets/TargetSelection.cs +++ b/libs/CheatEngine.SDK.Engine/Targets/TargetSelection.cs @@ -34,7 +34,9 @@ namespace CheatEngine.SDK.Engine.Targets; /// /// /// -/// isConnectedToCEServer() == true +/// +/// isConnectedToCEServer() == true +/// /// /// : a local PID and creation /// time do not describe a PID served by CEServer, so no lookup runs and no incarnation is emitted. diff --git a/libs/CheatEngine.SDK.Engine/Targets/TargetSelectionObservation.cs b/libs/CheatEngine.SDK.Engine/Targets/TargetSelectionObservation.cs index 73ebf36e..6b8cae4a 100644 --- a/libs/CheatEngine.SDK.Engine/Targets/TargetSelectionObservation.cs +++ b/libs/CheatEngine.SDK.Engine/Targets/TargetSelectionObservation.cs @@ -66,7 +66,8 @@ internal static TargetSelectionObservation Qualified(TargetProcessIncarnation in { return new TargetSelectionObservation(TargetSelectionObservationStatus.CurrentTargetQualified, TargetIdentityEvidence.CheatEngineSelectedProcessId | TargetIdentityEvidence.LocalBackendConfirmed | - TargetIdentityEvidence.LocalProcessStartTime, incarnation.ProcessId, incarnation, TargetBackend.LocalProcess); + TargetIdentityEvidence.LocalProcessStartTime, incarnation.ProcessId, incarnation, + TargetBackend.LocalProcess); } internal static TargetSelectionObservation NoTarget() diff --git a/libs/CheatEngine.SDK.Engine/Values/IndexBase.cs b/libs/CheatEngine.SDK.Engine/Values/IndexBase.cs index da7f5816..e69da332 100644 --- a/libs/CheatEngine.SDK.Engine/Values/IndexBase.cs +++ b/libs/CheatEngine.SDK.Engine/Values/IndexBase.cs @@ -69,7 +69,7 @@ public static int FromLuaKey(long luaKey) /// when the key is below one or the index would not fit an . public static bool TryFromLuaKey(long luaKey, out int zeroBasedIndex) { - if (luaKey < FirstLuaKey || luaKey > int.MaxValue + FirstLuaKey) + if (luaKey is < FirstLuaKey or > int.MaxValue + FirstLuaKey) { zeroBasedIndex = 0; return false; diff --git a/libs/CheatEngine.SDK.Engine/packages.lock.json b/libs/CheatEngine.SDK.Engine/packages.lock.json index 13ead563..323657c0 100644 --- a/libs/CheatEngine.SDK.Engine/packages.lock.json +++ b/libs/CheatEngine.SDK.Engine/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.Lifecycle.cs b/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.Lifecycle.cs index 9b904e6c..ec7dada4 100644 --- a/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.Lifecycle.cs +++ b/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.Lifecycle.cs @@ -471,7 +471,7 @@ private static bool Disable() return true; } - bool cleanupSucceeded = false; + bool cleanupSucceeded; try { RunDisable(context!, plugin); diff --git a/libs/CheatEngine.SDK.Hosting/Diagnostics/LoadIdentification.cs b/libs/CheatEngine.SDK.Hosting/Diagnostics/LoadIdentification.cs index 93c64b36..26be7211 100644 --- a/libs/CheatEngine.SDK.Hosting/Diagnostics/LoadIdentification.cs +++ b/libs/CheatEngine.SDK.Hosting/Diagnostics/LoadIdentification.cs @@ -47,10 +47,10 @@ internal static unsafe partial class LoadIdentification private const long MaxHashedFileBytes = 16 * 1024 * 1024; [GeneratedRegex("(? @@ -90,7 +90,7 @@ internal static string Build(ManagedExportedFunctions* exports, uint pluginId, T AppendLuaFields(builder); AppendCeFields(builder); AppendField(builder, "runtime", RuntimeInformation.FrameworkDescription); - AppendField(builder, "arch", RuntimeInformation.ProcessArchitecture.ToString(), last: true); + AppendField(builder, "arch", RuntimeInformation.ProcessArchitecture.ToString(), true); if (builder.Length > MaxEntryLength) { diff --git a/libs/CheatEngine.SDK.Hosting/packages.lock.json b/libs/CheatEngine.SDK.Hosting/packages.lock.json index 6aaef2a2..51af413a 100644 --- a/libs/CheatEngine.SDK.Hosting/packages.lock.json +++ b/libs/CheatEngine.SDK.Hosting/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/libs/CheatEngine.SDK.Lua.Interop/Api/LuaApi.Constants.cs b/libs/CheatEngine.SDK.Lua.Interop/Api/LuaApi.Constants.cs index 49aac567..ff800ec7 100644 --- a/libs/CheatEngine.SDK.Lua.Interop/Api/LuaApi.Constants.cs +++ b/libs/CheatEngine.SDK.Lua.Interop/Api/LuaApi.Constants.cs @@ -17,7 +17,7 @@ public static unsafe partial class LuaApi /// LUAL_NUMSIZES: the size fingerprint luaL_checkversion compares, /// sizeof(lua_Integer) * 16 + sizeof(lua_Number). /// - public const int LUAL_NUMSIZES = sizeof(lua_Integer) * 16 + sizeof(lua_Number); + public const int LUAL_NUMSIZES = (sizeof(lua_Integer) * 16) + sizeof(lua_Number); // ---- limits and pseudo-indices (luaconf.h) ------------------------------------------------------------------- diff --git a/libs/CheatEngine.SDK.Lua.Interop/CheatEngine.SDK.Lua.Interop.csproj b/libs/CheatEngine.SDK.Lua.Interop/CheatEngine.SDK.Lua.Interop.csproj index ac3825a3..df008f66 100644 --- a/libs/CheatEngine.SDK.Lua.Interop/CheatEngine.SDK.Lua.Interop.csproj +++ b/libs/CheatEngine.SDK.Lua.Interop/CheatEngine.SDK.Lua.Interop.csproj @@ -19,7 +19,7 @@ - + diff --git a/libs/CheatEngine.SDK.Lua.Interop/GlobalUsings.cs b/libs/CheatEngine.SDK.Lua.Interop/GlobalUsings.cs index 9ecce11a..f2afb927 100644 --- a/libs/CheatEngine.SDK.Lua.Interop/GlobalUsings.cs +++ b/libs/CheatEngine.SDK.Lua.Interop/GlobalUsings.cs @@ -5,6 +5,7 @@ // Scalars (luaconf.h of a default 64-bit build: LUA_INT_LONGLONG + LUA_REAL_DOUBLE, which is what Cheat Engine ships). // Function-pointer typedefs. Lua is cdecl; the convention is a no-op on x64 but matters on x86, so it is spelled out. + global using unsafe lua_Alloc = delegate* unmanaged[Cdecl]; global using unsafe lua_CFunction = delegate* unmanaged[Cdecl]; global using unsafe lua_Hook = diff --git a/libs/CheatEngine.SDK.Lua.Interop/packages.lock.json b/libs/CheatEngine.SDK.Lua.Interop/packages.lock.json index 75a512c2..cc7bf705 100644 --- a/libs/CheatEngine.SDK.Lua.Interop/packages.lock.json +++ b/libs/CheatEngine.SDK.Lua.Interop/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/libs/CheatEngine.SDK.Lua/CompilerServices/LuaCallSupport.cs b/libs/CheatEngine.SDK.Lua/CompilerServices/LuaCallSupport.cs index e1208da5..9c759add 100644 --- a/libs/CheatEngine.SDK.Lua/CompilerServices/LuaCallSupport.cs +++ b/libs/CheatEngine.SDK.Lua/CompilerServices/LuaCallSupport.cs @@ -153,7 +153,8 @@ public static bool TryReadOptional(LuaState state, int index, ou { if (index <= 0) { - throw new ArgumentOutOfRangeException(nameof(index), index, "An optional value is read at a positive, absolute index."); + throw new ArgumentOutOfRangeException(nameof(index), index, + "An optional value is read at a positive, absolute index."); } if (index > state.Top) diff --git a/libs/CheatEngine.SDK.Lua/Marshalling/Int64Marshaller.cs b/libs/CheatEngine.SDK.Lua/Marshalling/Int64Marshaller.cs index 44d2a1d7..9c2e0004 100644 --- a/libs/CheatEngine.SDK.Lua/Marshalling/Int64Marshaller.cs +++ b/libs/CheatEngine.SDK.Lua/Marshalling/Int64Marshaller.cs @@ -10,7 +10,8 @@ namespace CheatEngine.SDK.Lua.Marshalling; /// /// /// -/// Reading never lets a value pass through a lossy : an integer subtype reads exactly; a float +/// Reading never lets a value pass through a lossy : an integer subtype reads exactly; a +/// float /// reads only when it is integral and of magnitude below 2^53 (3.0 succeeds, 9007199254740992.0, /// 1e19 and 2.5 fail); a string reads only when it is a Lua integer numeral that fits 64 bits /// ("42", " 0x10 ", "-9223372036854775808" succeed; "3.0", "1e3", diff --git a/libs/CheatEngine.SDK.Lua/Marshalling/LuaIntegerReader.cs b/libs/CheatEngine.SDK.Lua/Marshalling/LuaIntegerReader.cs index 0e3440b3..da1ea1a2 100644 --- a/libs/CheatEngine.SDK.Lua/Marshalling/LuaIntegerReader.cs +++ b/libs/CheatEngine.SDK.Lua/Marshalling/LuaIntegerReader.cs @@ -82,7 +82,7 @@ private static bool AcceptConverted(LuaState state, int index, bool acceptIntege bool accepted = state.TypeOf(index) switch { // lua_tointegerx already refused a non-integral, infinite or NaN float and one outside the long range. - LuaType.Number => value > -FloatExactLimit && value < FloatExactLimit, + LuaType.Number => value is > -FloatExactLimit and < FloatExactLimit, LuaType.String => acceptIntegerNumerals && state.TryReadUtf8(index, out ReadOnlySpan text) && IsIntegerNumeral(text), _ => false @@ -150,6 +150,6 @@ private static int SkipSpaces(ReadOnlySpan text, int i) private static bool IsHexadecimalDigit(byte character) { return (character >= (byte) '0' && character <= (byte) '9') || ((character | 0x20) >= (byte) 'a' && - (character | 0x20) <= (byte) 'f'); + (character | 0x20) <= (byte) 'f'); } } diff --git a/libs/CheatEngine.SDK.Lua/Marshalling/LuaOptional.cs b/libs/CheatEngine.SDK.Lua/Marshalling/LuaOptional.cs index a2084e29..37fd0e07 100644 --- a/libs/CheatEngine.SDK.Lua/Marshalling/LuaOptional.cs +++ b/libs/CheatEngine.SDK.Lua/Marshalling/LuaOptional.cs @@ -59,7 +59,8 @@ public static LuaOptional Of(T value) /// . /// /// -/// The type is an ordinary (non-) struct, so a span cannot be optional; an optional text value +/// The type is an ordinary (non-) struct, so a span cannot be optional; an optional text +/// value /// is a LuaOptional<string>. Creating one allocates nothing beyond the value itself. /// /// diff --git a/libs/CheatEngine.SDK.Lua/Runtime/LuaAdmissionStatus.cs b/libs/CheatEngine.SDK.Lua/Runtime/LuaAdmissionStatus.cs index 0a9b5af9..69e993de 100644 --- a/libs/CheatEngine.SDK.Lua/Runtime/LuaAdmissionStatus.cs +++ b/libs/CheatEngine.SDK.Lua/Runtime/LuaAdmissionStatus.cs @@ -1,6 +1,9 @@ namespace CheatEngine.SDK.Lua.Runtime; -/// The factual reason did, or did not, admit an operation. +/// +/// The factual reason did, or did not, admit an +/// operation. +/// /// /// The zero value is ; a default value never reads as admitted. Callers must switch on this /// enum instead of parsing an exception message: throws a diff --git a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs index 9ed0d4a7..acaf947f 100644 --- a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs +++ b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs @@ -59,9 +59,13 @@ namespace CheatEngine.SDK.Lua.Runtime; /// policy from pointer inequality nor claims a live concurrency qualification. /// /// -/// Threading and Lua concurrency contract (ADR-07, unqualified pending a future host-based validation run; no -/// local CE qualification is currently available). The 2.0 default is : -/// a new -family call is refused with +/// +/// Threading and Lua concurrency contract (ADR-07, unqualified pending a future host-based validation run; no +/// local CE qualification is currently available). +/// +/// The 2.0 default is : +/// a new -family call is refused with +/// /// before the host's state provider ever runs, unless the calling thread is the host's captured main thread, the /// call is nested inside a Lua operation or callback already admitted on that thread, or it is the single /// documented default exception: the worker-side synchronize hand-off behind @@ -109,7 +113,6 @@ public static unsafe class LuaRuntime // The private-registry universe stamp (WI-3 / A08-21, A08-22, A20-Q17-3, A20-Q18-2). Key is unique per SDK copy and // never moves; s_stamped guards the one-time write per attachment so later admissions only verify it. - private static readonly nint s_stampKey = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(LuaRuntime), 1); private static int s_stamped; private static int s_externalResetDetected; @@ -130,7 +133,10 @@ public static unsafe class LuaRuntime internal static Action? OperationAdmissionClosedForTesting; /// The universe stamp's private-registry key. For tests only (distinctness from other private keys). - internal static nint StampKeyForTests => s_stampKey; + internal static nint StampKeyForTests + { + get; + } = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(LuaRuntime), 1); /// Gets a value indicating whether a host binding is attached. Lock-free; any thread. public static bool IsAttached => Read(ref s_services) is not null; @@ -260,7 +266,10 @@ public static bool TryAcquireOperation(out LuaRuntimeOperation operation) /// /// Non-throwing that reports the factual admission reason instead of a boolean. /// - /// The admitted operation when the result is ; default otherwise. + /// + /// The admitted operation when the result is ; default + /// otherwise. + /// /// The factual admission outcome. Never derive a reason from an exception message instead of this enum. [RequiresPluginEnabled] public static LuaAdmissionStatus TryAcquireOperationWithOutcome(out LuaRuntimeOperation operation) @@ -278,7 +287,8 @@ public static LuaAdmissionStatus TryAcquireOperationWithOutcome(out LuaRuntimeOp /// The result is selected while SOperationGate is held. In particular, an /// result cannot be reinterpreted as detached /// after an unsuccessful transition reopens admission: callback disposal must leave registry ownership with that - /// transition until a state has neutralized the Lua closure. + /// transition until a state has neutralized the Lua closure. + /// /// and are treated the same way by callers: /// never abandon the closure early from a thread or a universe the SDK does not currently trust. /// @@ -883,7 +893,7 @@ internal static void ExitOperation() // Evaluates the 2.0 admission policy under SOperationGate, entirely before services.Provider() ever runs on a // refused thread (pitfall 2): detached, external reset, transitioning and thread-not-admitted are all decided // without creating a coroutine. Only then is the provider called and the universe stamp verified. - private static unsafe LuaAdmissionStatus TryEnterProviderOperationWithOutcome(out LuaState state, + private static LuaAdmissionStatus TryEnterProviderOperationWithOutcome(out LuaState state, bool bypassThreadAdmission) { LuaAdmissionStatus gateStatus = TryEnterAdmissionGate(bypassThreadAdmission, out LuaHostServices? services); @@ -971,8 +981,8 @@ private static bool TryStampOrVerify(LuaState state) { if (Interlocked.CompareExchange(ref s_stamped, 1, 0) == 0) { - state.PushLightUserdata(s_stampKey); - state.RawSetPointer(LuaState.RegistryIndex, s_stampKey); + state.PushLightUserdata(StampKeyForTests); + state.RawSetPointer(LuaState.RegistryIndex, StampKeyForTests); return true; } @@ -982,8 +992,8 @@ private static bool TryStampOrVerify(LuaState state) // Raw, non-allocating, no metamethod (pitfall 6c): a plain rawgetp/touserdata/pop. ZeroAllocationTests gates this. private static bool CheckStamp(LuaState state) { - LuaType type = state.RawGetPointer(LuaState.RegistryIndex, s_stampKey); - bool matches = type == LuaType.LightUserdata && state.ToUserdata(-1) == s_stampKey; + LuaType type = state.RawGetPointer(LuaState.RegistryIndex, StampKeyForTests); + bool matches = type == LuaType.LightUserdata && state.ToUserdata(-1) == StampKeyForTests; state.Pop(1); return matches; } @@ -991,7 +1001,7 @@ private static bool CheckStamp(LuaState state) // Best-effort eager stamp at Attach (or after CompleteStateReset) time, on the state the binding already // provides. Deferred to the first admitted acquisition when the provider yields no state yet (pitfall 6a: tests // may attach with a provider returning null). - private static unsafe void StampIfStateAvailable(LuaHostServices services) + private static void StampIfStateAvailable(LuaHostServices services) { lua_State* l = services.Provider(); if (l is null) @@ -1004,8 +1014,8 @@ private static unsafe void StampIfStateAvailable(LuaHostServices services) { if (Interlocked.CompareExchange(ref s_stamped, 1, 0) == 0) { - state.PushLightUserdata(s_stampKey); - state.RawSetPointer(LuaState.RegistryIndex, s_stampKey); + state.PushLightUserdata(StampKeyForTests); + state.RawSetPointer(LuaState.RegistryIndex, StampKeyForTests); } } catch @@ -1021,7 +1031,7 @@ private static unsafe void StampIfStateAvailable(LuaHostServices services) // BeginStateReset) would otherwise unref and neutralize against. Never mutates anything and never releases a // reference: a mismatch means the whole registry belongs to a different Lua universe (A08-22), so the caller // must abandon instead of unregistering into it. - private static unsafe bool DetectExternalResetForDetach(LuaHostServices services, out LuaState state) + private static bool DetectExternalResetForDetach(LuaHostServices services, out LuaState state) { if (Read(ref s_externalResetDetected) != 0) { diff --git a/libs/CheatEngine.SDK.Lua/Runtime/LuaThreadAdmission.cs b/libs/CheatEngine.SDK.Lua/Runtime/LuaThreadAdmission.cs index c8022b97..9607574b 100644 --- a/libs/CheatEngine.SDK.Lua/Runtime/LuaThreadAdmission.cs +++ b/libs/CheatEngine.SDK.Lua/Runtime/LuaThreadAdmission.cs @@ -20,7 +20,8 @@ public enum LuaThreadAdmission /// /// The conservative default: a new -family call is admitted only on /// the host's captured main thread, inside a host-invoked callback already running on the calling thread, or - /// through the single documented synchronize hand-off (). + /// through the single documented synchronize hand-off ( + /// ). /// MainThreadOnly = 0, diff --git a/libs/CheatEngine.SDK.Lua/State/LuaState.Types.cs b/libs/CheatEngine.SDK.Lua/State/LuaState.Types.cs index 59e62d9a..800fc993 100644 --- a/libs/CheatEngine.SDK.Lua/State/LuaState.Types.cs +++ b/libs/CheatEngine.SDK.Lua/State/LuaState.Types.cs @@ -48,7 +48,7 @@ public ReadOnlySpan TypeName(int index) /// One C API call; never raises for a tag of . public ReadOnlySpan TypeName(LuaType type) { - Debug.Assert(type >= LuaType.None && type <= LuaType.Thread, "Not a Lua 5.3 type tag."); + Debug.Assert(type is >= LuaType.None and <= LuaType.Thread, "Not a Lua 5.3 type tag."); return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lua_typename(Pointer, (int) type)); } diff --git a/libs/CheatEngine.SDK.Lua/packages.lock.json b/libs/CheatEngine.SDK.Lua/packages.lock.json index a1e55139..01aa6303 100644 --- a/libs/CheatEngine.SDK.Lua/packages.lock.json +++ b/libs/CheatEngine.SDK.Lua/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/AnalyzerReleases.Unshipped.md b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/AnalyzerReleases.Unshipped.md index 7b32c473..231c0255 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/AnalyzerReleases.Unshipped.md +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/AnalyzerReleases.Unshipped.md @@ -4,7 +4,7 @@ ### New Rules Rule ID | Category | Severity | Notes ------------|---------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-----------|---------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------- CESDK3001 | CheatEngine.SDK.EngineApi | Error | EngineApiGenerator, invalid curated API specification, [Documentation](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/CESDK3001.md) CESDK3002 | CheatEngine.SDK.EngineApi | Error | EngineApiGenerator, generated-identity conflict, [Documentation](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/CESDK3002.md) CESDK3003 | CheatEngine.SDK.EngineApi | Error | EngineApiGenerator, missing ce77 contract, [Documentation](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/CESDK3003.md) diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecFileParser.cs b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecFileParser.cs index ed0183a6..994155c2 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecFileParser.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecFileParser.cs @@ -527,7 +527,7 @@ private static bool IsFourPartVersion(string value) continue; } - if (character < '0' || character > '9') + if (character is < '0' or > '9') { return false; } @@ -681,10 +681,12 @@ private static bool TrySetEntryField(EntryFields fields, HashSet singula case "return": return TrySetReturn(fields, singular, field, issues); case "arg": - fields.ArgumentTokens.Add(new SpecToken(field.Line, field.ValueColumn, field.Value, TokenRole.Argument)); + fields.ArgumentTokens.Add(new SpecToken(field.Line, field.ValueColumn, field.Value, + TokenRole.Argument)); return true; case "opt": - fields.ArgumentTokens.Add(new SpecToken(field.Line, field.ValueColumn, field.Value, TokenRole.Optional)); + fields.ArgumentTokens.Add(new SpecToken(field.Line, field.ValueColumn, field.Value, + TokenRole.Optional)); return true; case "fixed": fields.ArgumentTokens.Add(new SpecToken(field.Line, field.ValueColumn, field.Value, TokenRole.Fixed)); @@ -840,7 +842,8 @@ private static bool ValidateResultOrder(List tokens, LuaCallForm form } else if (token.Role == TokenRole.Argument && sawOptional) { - problem = "is a required 'result' after an 'opt-result': optional results come after every required one"; + problem = + "is a required 'result' after an 'opt-result': optional results come after every required one"; } else if (token.Role == TokenRole.Rest && form != LuaCallForm.Outcome) { @@ -907,8 +910,10 @@ private static bool TryParseReturnKind( if (sawOptional && token.Role != TokenRole.Optional) { issues.Add(new SpecIssue(token.Line, - "'" + token.Value + "' follows an 'opt' argument: only more 'opt' arguments may follow one, because Lua " - + "cannot receive an argument after an omitted one.", token.Column, SpecIssueKind.OptionalArgument)); + "'" + token.Value + + "' follows an 'opt' argument: only more 'opt' arguments may follow one, because Lua " + + "cannot receive an argument after an omitted one.", token.Column, + SpecIssueKind.OptionalArgument)); return null; } @@ -946,7 +951,8 @@ private static bool TryParseReturnKind( if (nullable || !LuaValueKinds.CanBeOptional(kind)) { issues.Add(new SpecIssue(token.Line, - "'" + kindToken + "' cannot be an 'opt' kind: nil is the Nil state of LuaOptional, and a span cannot be " + "'" + kindToken + + "' cannot be an 'opt' kind: nil is the Nil state of LuaOptional, and a span cannot be " + "optional; use 'string' for optional text.", token.Column, SpecIssueKind.OptionalArgument)); return null; } @@ -1022,7 +1028,8 @@ private static bool TryParseReturnKind( return LuaResultModel.Optional(kind, name); case TokenRole.Rest when !LuaValueKinds.CanBeVariadicElement(kind) || kind == LuaValueKind.Address: issues.Add(new SpecIssue(token.Line, - "'" + kindToken + "' cannot be a 'rest' kind: expected 'int32', 'int64', 'single', 'double' or 'boolean'.", + "'" + kindToken + + "' cannot be a 'rest' kind: expected 'int32', 'int64', 'single', 'double' or 'boolean'.", token.Column, SpecIssueKind.ResultShape)); return null; case TokenRole.Rest: diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecIdentifiers.cs b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecIdentifiers.cs index ee692c81..d845190a 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecIdentifiers.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/Parsing/SpecIdentifiers.cs @@ -86,7 +86,7 @@ public static string Escape(string identifier) private static bool IsStart(char c) { - return c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or '_'; + return c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '_'; } private static bool IsPart(char c) diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/README.md b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/README.md index b2328a38..f8dc1414 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/README.md +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/README.md @@ -38,15 +38,15 @@ additional file: Only files explicitly included as `AdditionalFiles` participate in one generator invocation. Every production spec declares `contract: ce77` and parses without an issue, and they fall in three groups: -| Spec | Status | -|---------------------------------------------------|------------------------------------------------------------------------------------------------------------| -| `memory-scalars.cheatengine-sdk-api.txt` | Wired: generates `CheatEngine.SDK.Engine.Generated.MemoryScalars`. | -| `runtime-capabilities.cheatengine-sdk-api.txt` | Reviewed, not wired yet: runtime capability probes (the runtime lot wires it). | -| `allocation-protection.cheatengine-sdk-api.txt` | Reviewed, not wired: the public `Address`/`TargetAllocationRequest` contract is hand-written in Engine. | -| `modules-symbols-regions.cheatengine-sdk-api.txt` | Reviewed, not wired: the public inspection wrapper uses the same global directly. | -| `addresslist-memoryrecord.cheatengine-sdk-api.txt`| Header-only reservation: needs the object grammar. | -| `aob-stringlist.cheatengine-sdk-api.txt` | Header-only reservation: needs the object-result grammar. | -| `memscan-foundlist.cheatengine-sdk-api.txt` | Header-only reservation: needs the object-method grammar. | +| Spec | Status | +|----------------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `memory-scalars.cheatengine-sdk-api.txt` | Wired: generates `CheatEngine.SDK.Engine.Generated.MemoryScalars`. | +| `runtime-capabilities.cheatengine-sdk-api.txt` | Reviewed, not wired yet: runtime capability probes (the runtime lot wires it). | +| `allocation-protection.cheatengine-sdk-api.txt` | Reviewed, not wired: the public `Address`/`TargetAllocationRequest` contract is hand-written in Engine. | +| `modules-symbols-regions.cheatengine-sdk-api.txt` | Reviewed, not wired: the public inspection wrapper uses the same global directly. | +| `addresslist-memoryrecord.cheatengine-sdk-api.txt` | Header-only reservation: needs the object grammar. | +| `aob-stringlist.cheatengine-sdk-api.txt` | Header-only reservation: needs the object-result grammar. | +| `memscan-foundlist.cheatengine-sdk-api.txt` | Header-only reservation: needs the object-method grammar. | A reviewed or reserved spec is not silently generated, and its presence does not imply that a wrapper exists. The number of entries is not a coverage measure: the four `MemoryScalars` entries are four explicit contracts, not the Lua @@ -76,28 +76,28 @@ A spec file is plain text made of `key: value` lines. Blank lines separate block every later block is one entry. A line that starts with `#` is a comment and never separates blocks. Indentation and CRLF line endings are tolerated. -| Key | Block | Count | Meaning | -|----------------|--------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `namespace` | header | 1 | Namespace of the generated type. An empty value means the global namespace. | -| `type` | header | 1 | Name of the one `public static partial class` the file emits. | -| `contract` | header | 1 with entries | `ce77`: the mandatory, machine-validated CE 7.7 evidence contract below (`CESDK3003` when a file with entries omits it). | -| `provenance` | header | with `contract` | A proof status followed by `: ` and an immutable source reference. | -| `minimum-ce` | header | with `contract` | Exact four-part minimum CE version, for example `7.7.0.10621`. | -| `architecture` | header | with `contract` | `x64`; this generator makes no public target-address claim on another architecture. | -| `thread` | header | with `contract` | `any`, `main`, or `unknown`; `unknown` records absent proof rather than granting thread safety. | -| `ownership` | header | with `contract` | `none`, `borrowed`, or `owned`. Object contracts remain deliberately outside the scalar grammar. | -| `global` | entry | 1 | The Lua global to call: an ASCII identifier that is not a Lua 5.3 reserved word. | -| `method` | entry | 1 | The C# method name. A C# reserved word gets an `@` prefix. | +| Key | Block | Count | Meaning | +|----------------|--------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `namespace` | header | 1 | Namespace of the generated type. An empty value means the global namespace. | +| `type` | header | 1 | Name of the one `public static partial class` the file emits. | +| `contract` | header | 1 with entries | `ce77`: the mandatory, machine-validated CE 7.7 evidence contract below (`CESDK3003` when a file with entries omits it). | +| `provenance` | header | with `contract` | A proof status followed by `: ` and an immutable source reference. | +| `minimum-ce` | header | with `contract` | Exact four-part minimum CE version, for example `7.7.0.10621`. | +| `architecture` | header | with `contract` | `x64`; this generator makes no public target-address claim on another architecture. | +| `thread` | header | with `contract` | `any`, `main`, or `unknown`; `unknown` records absent proof rather than granting thread safety. | +| `ownership` | header | with `contract` | `none`, `borrowed`, or `owned`. Object contracts remain deliberately outside the scalar grammar. | +| `global` | entry | 1 | The Lua global to call: an ASCII identifier that is not a Lua 5.3 reserved word. | +| `method` | entry | 1 | The C# method name. A C# reserved word gets an `@` prefix. | | `form` | entry | 1 | `try` returns `bool` and writes `out` results. `outcome` returns `LuaOperationStatus` and writes `out` results. `throwing` returns the value, or `void`, and raises `LuaException` when the Lua call fails. | -| `doc` | entry | 1 | One line of original English, emitted as the XML ``. Never copy Cheat Engine documentation. | -| `arg` | entry | 0 or more | `name:kind`, one pushed argument. `arg`, `fixed` and `opt` keep their textual order, which is the push order. | -| `fixed` | entry | 0 or more | `boolean:true` or `boolean:false`, one host-required Lua argument omitted from the C# signature. | -| `opt` | entry | 0 or more, last | `name:kind`, one `LuaOptional` argument: omitted (not pushed), `Nil` (pushed as `nil`) or a value. Only `opt` may follow (`CESDK3004`). | -| `result` | entry | `try`: 1 or more | `name:kind`, one `out` result, in read order. Not allowed in a `throwing` entry. | -| `opt-result` | entry | 0 or more | `name:kind`, one `out LuaOptional` result after the `result` values: a position Lua did not return is omitted (`CESDK3005`). | -| `rest` | entry | `outcome`: 0 or 1 | `name:kind`, the variadic tail `Span name, out int nameCount`, last; kinds `int32`, `int64`, `single`, `double`, `boolean`. | -| `return` | entry | `throwing`: 0 or 1 | The kind of the returned value. Omit it for `void`. Not allowed in a `try` entry. | -| `nil` | entry | with `contract` | `none`, `absence`, `expected-failure`, or `lua-error`: CE result semantics after a protected call succeeds. | +| `doc` | entry | 1 | One line of original English, emitted as the XML ``. Never copy Cheat Engine documentation. | +| `arg` | entry | 0 or more | `name:kind`, one pushed argument. `arg`, `fixed` and `opt` keep their textual order, which is the push order. | +| `fixed` | entry | 0 or more | `boolean:true` or `boolean:false`, one host-required Lua argument omitted from the C# signature. | +| `opt` | entry | 0 or more, last | `name:kind`, one `LuaOptional` argument: omitted (not pushed), `Nil` (pushed as `nil`) or a value. Only `opt` may follow (`CESDK3004`). | +| `result` | entry | `try`: 1 or more | `name:kind`, one `out` result, in read order. Not allowed in a `throwing` entry. | +| `opt-result` | entry | 0 or more | `name:kind`, one `out LuaOptional` result after the `result` values: a position Lua did not return is omitted (`CESDK3005`). | +| `rest` | entry | `outcome`: 0 or 1 | `name:kind`, the variadic tail `Span name, out int nameCount`, last; kinds `int32`, `int64`, `single`, `double`, `boolean`. | +| `return` | entry | `throwing`: 0 or 1 | The kind of the returned value. Omit it for `void`. Not allowed in a `try` entry. | +| `nil` | entry | with `contract` | `none`, `absence`, `expected-failure`, or `lua-error`: CE result semantics after a protected call succeeds. | The kinds are `int32`, `int64`, `single`, `double`, `boolean`, `address`, `utf8`, `string` and `string?`. A `utf8` value is valid only as an argument, because a span result would dangle once the wrapper restores the stack. `utf8` and @@ -172,20 +172,20 @@ A19-23). Three independent checks therefore exist: ## Promise -| You can rely on | Backed by | -|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| -| The generator never ships. | `src/CheatEngine.SDK/CheatEngine.SDK.csproj` packs only the analyzer references it marks `PackAsAnalyzer`, and it does not reference this project. | -| Every invalid spec issue is a localized `CESDK3001` error; an unrelated valid entry still emits. | `Parsing/SpecFileParserTests.cs`, `Generator/DiagnosticsTests.cs` | -| A spec with entries must declare `contract: ce77` (`CESDK3003`); every production spec parses without an issue, cites the pinned `celua.txt` SHA-256, claims no unproven thread affinity, and is wired or reviewed. | `Parsing/ProductionSpecsTests.cs`, `Spec_with_entries_without_contract_reports_the_missing_ce77_contract` | -| `opt` arguments are omitted, `nil` or pushed exactly as declared; `opt-result` distinguishes zero results from `nil`; `rest` copies every value or reports the needed capacity; an absent global is `GlobalUnavailable` in the `outcome` form. | `EndToEnd/EngineApiOptionalEndToEndTests.cs`, `Parsing/SpecFileParserTests.cs` (`CESDK3004`/`CESDK3005` rows) | -| The wired `MemoryScalars` output is byte-identical to its committed text; a reformatted spec (spaces, comments, CRLF) keeps the same contract and call model. | `Memory_scalars_output_is_unchanged_by_the_new_grammar`, `Reformatting_a_spec_keeps_its_contract_and_call_model` | -| Every EngineApi diagnostic has a documentation page, a help link and a release-tracking row. | `Generator/EngineApiDiagnosticCatalogTests.cs` | -| A ce77 spec carries validated provenance/version/architecture/thread/ownership/return/nil facts on every entry and projects them into XML documentation. | `Parsing/SpecFileParserTests.cs`, `Generator/EmissionTests.cs` | -| A spec file exclusively owns its generated type; duplicate type/member/cache identities are `CESDK3002` errors at each exact field and emit neither file. | `Generator/DiagnosticsTests.cs` | -| Emitted code compiles without errors or warnings against the real `CheatEngine.SDK.Annotations`, `CheatEngine.SDK.Lua.Interop` and `CheatEngine.SDK.Lua`. | `Generator/EmissionTests.cs` | -| Editing one spec file re-emits only that file. An unrelated compilation edit recomputes nothing. | `Generator/IncrementalityTests.cs` | -| No parsed model in the pipeline holds a Roslyn object. | `Pipeline_step_values_hold_no_roslyn_objects` in `Generator/IncrementalityTests.cs` | -| Wrappers leave the Lua stack as they found it, allocate nothing on the warm success path, and throw `InvalidOperationException` while no runtime is attached. | `EndToEnd/MemoryScalarsEndToEndTests.cs`, run against stand-in Lua globals | +| You can rely on | Backed by | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| The generator never ships. | `src/CheatEngine.SDK/CheatEngine.SDK.csproj` packs only the analyzer references it marks `PackAsAnalyzer`, and it does not reference this project. | +| Every invalid spec issue is a localized `CESDK3001` error; an unrelated valid entry still emits. | `Parsing/SpecFileParserTests.cs`, `Generator/DiagnosticsTests.cs` | +| A spec with entries must declare `contract: ce77` (`CESDK3003`); every production spec parses without an issue, cites the pinned `celua.txt` SHA-256, claims no unproven thread affinity, and is wired or reviewed. | `Parsing/ProductionSpecsTests.cs`, `Spec_with_entries_without_contract_reports_the_missing_ce77_contract` | +| `opt` arguments are omitted, `nil` or pushed exactly as declared; `opt-result` distinguishes zero results from `nil`; `rest` copies every value or reports the needed capacity; an absent global is `GlobalUnavailable` in the `outcome` form. | `EndToEnd/EngineApiOptionalEndToEndTests.cs`, `Parsing/SpecFileParserTests.cs` (`CESDK3004`/`CESDK3005` rows) | +| The wired `MemoryScalars` output is byte-identical to its committed text; a reformatted spec (spaces, comments, CRLF) keeps the same contract and call model. | `Memory_scalars_output_is_unchanged_by_the_new_grammar`, `Reformatting_a_spec_keeps_its_contract_and_call_model` | +| Every EngineApi diagnostic has a documentation page, a help link and a release-tracking row. | `Generator/EngineApiDiagnosticCatalogTests.cs` | +| A ce77 spec carries validated provenance/version/architecture/thread/ownership/return/nil facts on every entry and projects them into XML documentation. | `Parsing/SpecFileParserTests.cs`, `Generator/EmissionTests.cs` | +| A spec file exclusively owns its generated type; duplicate type/member/cache identities are `CESDK3002` errors at each exact field and emit neither file. | `Generator/DiagnosticsTests.cs` | +| Emitted code compiles without errors or warnings against the real `CheatEngine.SDK.Annotations`, `CheatEngine.SDK.Lua.Interop` and `CheatEngine.SDK.Lua`. | `Generator/EmissionTests.cs` | +| Editing one spec file re-emits only that file. An unrelated compilation edit recomputes nothing. | `Generator/IncrementalityTests.cs` | +| No parsed model in the pipeline holds a Roslyn object. | `Pipeline_step_values_hold_no_roslyn_objects` in `Generator/IncrementalityTests.cs` | +| Wrappers leave the Lua stack as they found it, allocate nothing on the warm success path, and throw `InvalidOperationException` while no runtime is attached. | `EndToEnd/MemoryScalarsEndToEndTests.cs`, run against stand-in Lua globals | ## Run the tests diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/packages.lock.json b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/packages.lock.json index 64d57ef4..50d815cb 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/packages.lock.json +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EngineApi/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.EntryPoint/packages.lock.json b/source-generators/CheatEngine.SDK.SourceGenerators.EntryPoint/packages.lock.json index 64d57ef4..50d815cb 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.EntryPoint/packages.lock.json +++ b/source-generators/CheatEngine.SDK.SourceGenerators.EntryPoint/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectMethodParser.cs b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectMethodParser.cs index cc8147cd..e72de6bd 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectMethodParser.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectMethodParser.cs @@ -80,8 +80,8 @@ private static bool TryDescribe(IMethodSymbol method, MethodDeclarationSyntax? d valid &= DescribeParameters(method.Parameters, arguments, results); LuaCallForm form = results.Count == 0 ? LuaCallForm.Throwing : LuaCallForm.Try; - LuaValueKind? returnKind = null; - bool returnNullable = false; + LuaValueKind? returnKind; + bool returnNullable; valid &= TryDescribeReturn(method, form, out returnKind, out returnNullable); model = new LuaObjectMethodModel( diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectPropertyParser.cs b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectPropertyParser.cs index 5e3513fa..4af51e2a 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectPropertyParser.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/Parsing/LuaObjectPropertyParser.cs @@ -60,11 +60,11 @@ public static LuaObjectPropertyModel Parse(GeneratorAttributeSyntaxContext conte private static bool TryDescribe(IPropertySymbol property, PropertyDeclarationSyntax? declaration, out LuaObjectPropertyModel model) { - LuaValueKind kind = default; - bool isNullable = false; - string modifiers = string.Empty; + LuaValueKind kind; + bool isNullable; bool typeIsSupported = LuaValueKindMapper.TryMap(property.Type, out kind, out isNullable) && LuaValueKinds.CanBeResult(kind); + string modifiers; bool definitionIsSupported = IsSupportedDefinition(property, declaration, typeIsSupported, out modifiers); bool accessorsAreSupported = TryDescribeAccessors(declaration, out bool hasGetter, out string getterModifiers, diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/README.md b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/README.md index fe493eb5..be2cfeb7 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/README.md +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/README.md @@ -72,15 +72,15 @@ per function. `MyTrainer.Bindings.LuaGlobals.g.cs` holds one cached `LuaRef` per ## Supported shapes -| Lua type | C# types | Allowed as | -|-----------------------------|------------------------------------------------------------|--------------------------------------------------------------------------------| -| integer | `int`, `long`, `nuint` | arguments and results | -| number | `float`, `double` | arguments and results | -| boolean | `bool` | arguments and results | -| string | `string`, `string?` | arguments and results | -| string | `ReadOnlySpan` | arguments, and `[LuaFunction]` results | -| any of the above, or absent | `LuaOptional`, `T` one of the types above except spans | trailing `[LuaGlobal]` and `[LuaFunction]` arguments; trailing `out` results | -| several integers/numbers | `Span values, out int count`, `T` a scalar type above | the last results of an Outcome-form `[LuaGlobal]` | +| Lua type | C# types | Allowed as | +|-----------------------------|-----------------------------------------------------------|------------------------------------------------------------------------------| +| integer | `int`, `long`, `nuint` | arguments and results | +| number | `float`, `double` | arguments and results | +| boolean | `bool` | arguments and results | +| string | `string`, `string?` | arguments and results | +| string | `ReadOnlySpan` | arguments, and `[LuaFunction]` results | +| any of the above, or absent | `LuaOptional`, `T` one of the types above except spans | trailing `[LuaGlobal]` and `[LuaFunction]` arguments; trailing `out` results | +| several integers/numbers | `Span values, out int count`, `T` a scalar type above | the last results of an Outcome-form `[LuaGlobal]` | A `[LuaGlobal]` method has one of three forms: @@ -139,8 +139,8 @@ helper, as above. That keeps the public `int` API from receiving an unsigned val [`CESDK2011`](../../analyzers/docs/CESDK2011.md)). A variadic `Span values, out int count` pair is the last result of an Outcome form, one per method (`CESDK2011`). `LuaOptional` and `LuaOperationStatus` must be the SDK types; a look-alike with the same name is refused ([`CESDK2012`](../../analyzers/docs/CESDK2012.md)). -- `LuaOptional` is not supported on `[LuaMethod]` and `[LuaProperty]` members yet - ([`CESDK2013`](../../analyzers/docs/CESDK2013.md)), nor with `ReadOnlySpan`. +- `LuaOptional` is not supported on `[LuaMethod]` and `[LuaProperty]` members yet ([ + `CESDK2013`](../../analyzers/docs/CESDK2013.md)), nor with `ReadOnlySpan`. - Integer, 64-bit and address values never pass through a `double`: a float at or above 2^53 is refused, not rounded (see the marshaller policy of [`CheatEngine.SDK.Lua`](../../libs/CheatEngine.SDK.Lua/README.md)). - There are no table or object results: a table where a scalar is declared is `InvalidResult`. diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/packages.lock.json b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/packages.lock.json index 64d57ef4..50d815cb 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/packages.lock.json +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBindings/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs index 52ab476a..1a4f4cf0 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/Catalog/ProtectedOperationCatalogParser.cs @@ -446,7 +446,7 @@ private static bool IsPascalIdentifier(string value) for (int i = 1; i < value.Length; i++) { char character = value[i]; - if (character is not (>= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9')) + if (character is not ((>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or (>= '0' and <= '9'))) { return false; } @@ -932,7 +932,7 @@ private void SkipWhitespace() while (_position < _text.Length) { char character = _text[_position]; - if (character != ' ' && character != '\t' && character != '\r' && character != '\n') + if (character is not ' ' and not '\t' and not '\r' and not '\n') { return; } diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/packages.lock.json b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/packages.lock.json index b52c3444..7109ba3b 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/packages.lock.json +++ b/source-generators/CheatEngine.SDK.SourceGenerators.LuaBridgeContract/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/EquatableArray.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/EquatableArray.cs index 0aedcf79..735067c5 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/EquatableArray.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/EquatableArray.cs @@ -129,7 +129,7 @@ public override int GetHashCode() for (int i = 0; i < items.Length; i++) { T? item = items[i]; - hash = unchecked(hash * 31 + (item is null ? 0 : comparer.GetHashCode(item))); + hash = unchecked((hash * 31) + (item is null ? 0 : comparer.GetHashCode(item))); } return hash; diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/HintNames.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/HintNames.cs index 78e634dd..a89502bd 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/HintNames.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/HintNames.cs @@ -129,7 +129,7 @@ private static string Build(string typeName, string suffix) bool replaced = false; foreach (char c in typeName) { - if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9' or '.' or '_') + if (c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or (>= '0' and <= '9') or '.' or '_') { builder.Append(c); } diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaGlobalShape.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaGlobalShape.cs index 75182c9f..e6bb03d1 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaGlobalShape.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaGlobalShape.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaValueKindMapper.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaValueKindMapper.cs index 793432c6..61f8a1c7 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaValueKindMapper.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaBindings/Parsing/LuaValueKindMapper.cs @@ -77,7 +77,10 @@ public static bool TryMap(ITypeSymbol type, out LuaValueKind kind, out bool isNu /// an optional, so object members ([LuaMethod], [LuaProperty]) keep refusing it. /// /// The parameter, result or return type. - /// The resolved CheatEngine.SDK.Lua.Marshalling.LuaOptional`1, or . + /// + /// The resolved CheatEngine.SDK.Lua.Marshalling.LuaOptional`1, or + /// . + /// /// The kind of T when the result is . public static LuaOptionalUse ClassifyOptional(ITypeSymbol type, INamedTypeSymbol? luaOptional, out LuaValueKind inner) @@ -99,7 +102,10 @@ public static LuaOptionalUse ClassifyOptional(ITypeSymbol type, INamedTypeSymbol : LuaOptionalUse.Unsupported; } - /// Whether is System.Span<T> for a T other than . + /// + /// Whether is System.Span<T> for a T other than + /// . + /// /// The parameter type. /// The element type when the result is . public static bool IsSpanOfOther(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? element) diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaArgumentModel.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaArgumentModel.cs index e8e5ae51..131f9a09 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaArgumentModel.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaArgumentModel.cs @@ -59,12 +59,6 @@ public LuaArgumentModel(string name, LuaValueKind kind, bool isNullable, bool is { } - /// An optional LuaOptional<T> argument of a built-in kind. - public static LuaArgumentModel Optional(string name, LuaValueKind kind) - { - return new LuaArgumentModel(name, kind, false, IsOptional: true); - } - /// Whether this value is pushed directly instead of being supplied by a wrapper parameter. public bool IsFixed => FixedValue is not null; @@ -83,4 +77,10 @@ public static LuaArgumentModel Optional(string name, LuaValueKind kind) /// The Lua-facing expected type in a generated bad-argument message. public string ExpectedArgumentTypeName => CustomMarshaller?.ExpectedTypeName ?? LuaValueKinds.ExpectedArgument(Kind); + + /// An optional LuaOptional<T> argument of a built-in kind. + public static LuaArgumentModel Optional(string name, LuaValueKind kind) + { + return new LuaArgumentModel(name, kind, false, IsOptional: true); + } } diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaNames.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaNames.cs index 23ce6c45..e8da28ef 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaNames.cs +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaNames.cs @@ -61,7 +61,7 @@ public static bool IsReservedWord(string name) private static bool IsIdentifierStart(char c) { - return c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or '_'; + return c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '_'; } private static bool IsIdentifierPart(char c) diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/packages.lock.json b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/packages.lock.json index b52c3444..7109ba3b 100644 --- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/packages.lock.json +++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/packages.lock.json @@ -4,9 +4,9 @@ ".NETStandard,Version=v2.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", diff --git a/src/CheatEngine.SDK/CompatibilitySuppressions.xml b/src/CheatEngine.SDK/CompatibilitySuppressions.xml index bbb20b27..a77ae4cf 100644 --- a/src/CheatEngine.SDK/CompatibilitySuppressions.xml +++ b/src/CheatEngine.SDK/CompatibilitySuppressions.xml @@ -1,67 +1,67 @@ - - - CP0002 - F:CheatEngine.SDK.Abi.Native.AddressListPluginInit.Callback - lib/net10.0/CheatEngine.SDK.Abi.dll - lib/net10.0/CheatEngine.SDK.Abi.dll - true - - - CP0002 - F:CheatEngine.SDK.Abi.Native.DisassemblerContextPluginInit.Callback - lib/net10.0/CheatEngine.SDK.Abi.dll - lib/net10.0/CheatEngine.SDK.Abi.dll - true - - - CP0002 - M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.#ctor(System.Boolean,System.Boolean) - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0002 - M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.Deconstruct(System.Boolean@,System.Boolean@) - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0002 - M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.get_UseHostSymbolTable - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0002 - M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.set_UseHostSymbolTable(System.Boolean) - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0011 - F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.DestinationTooSmall - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0011 - F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.InvalidResult - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - - CP0011 - F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.WriteFailed - lib/net10.0/CheatEngine.SDK.Engine.dll - lib/net10.0/CheatEngine.SDK.Engine.dll - true - - \ No newline at end of file + + + CP0002 + F:CheatEngine.SDK.Abi.Native.AddressListPluginInit.Callback + lib/net10.0/CheatEngine.SDK.Abi.dll + lib/net10.0/CheatEngine.SDK.Abi.dll + true + + + CP0002 + F:CheatEngine.SDK.Abi.Native.DisassemblerContextPluginInit.Callback + lib/net10.0/CheatEngine.SDK.Abi.dll + lib/net10.0/CheatEngine.SDK.Abi.dll + true + + + CP0002 + M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.#ctor(System.Boolean,System.Boolean) + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0002 + M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.Deconstruct(System.Boolean@,System.Boolean@) + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0002 + M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.get_UseHostSymbolTable + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0002 + M:CheatEngine.SDK.Engine.Inspection.AddressResolutionOptions.set_UseHostSymbolTable(System.Boolean) + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0011 + F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.DestinationTooSmall + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0011 + F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.InvalidResult + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + + CP0011 + F:CheatEngine.SDK.Engine.Memory.MemoryAccessFailure.WriteFailed + lib/net10.0/CheatEngine.SDK.Engine.dll + lib/net10.0/CheatEngine.SDK.Engine.dll + true + + diff --git a/src/CheatEngine.SDK/README.md b/src/CheatEngine.SDK/README.md index 6c9f6aeb..4b5ecca6 100644 --- a/src/CheatEngine.SDK/README.md +++ b/src/CheatEngine.SDK/README.md @@ -137,10 +137,12 @@ success is never a Cheat Engine load success.** | x86 or ARM64 host | Not supported | Cheat Engine's native loader removes a plugin with -`FreeLibrary`, and [Microsoft's Native AOT library guidance](https://learn.microsoft.com/dotnet/core/deploying/native-aot/libraries) +`FreeLibrary`, +and [Microsoft's Native AOT library guidance](https://learn.microsoft.com/dotnet/core/deploying/native-aot/libraries) states that .NET does not support unloading a NativeAOT library that way; only `UnmanagedCallersOnly` methods of the published assembly become native exports, so the package itself can never add classic `CEPlugin_*` exports for a -consumer. See the [full restrictions, evidence and diagnostics](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/libs/CheatEngine.SDK.Abi/README.md) +consumer. See +the [full restrictions, evidence and diagnostics](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/libs/CheatEngine.SDK.Abi/README.md) (`CESDK9102`, `CESDK0006`). ## Requirements diff --git a/src/CheatEngine.SDK/packages.lock.json b/src/CheatEngine.SDK/packages.lock.json index 79b82719..394ebff1 100644 --- a/src/CheatEngine.SDK/packages.lock.json +++ b/src/CheatEngine.SDK/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Abi.Tests/AbiBoundaryValueTests.cs b/tests/CheatEngine.SDK.Abi.Tests/AbiBoundaryValueTests.cs index df43b2a7..eaf4a97b 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/AbiBoundaryValueTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/AbiBoundaryValueTests.cs @@ -35,7 +35,8 @@ public void Bool32_results_2_and_0xFFFFFFFF_read_as_true_through_every_lifecycle (delegate* unmanaged[Stdcall]) &RawGetVersion; record.EnablePlugin = (delegate* unmanaged[Stdcall]) (delegate* unmanaged[Stdcall]) &RawEnablePlugin; - record.DisablePlugin = (delegate* unmanaged[Stdcall]) (delegate* unmanaged[Stdcall]) &RawDisablePlugin; + record.DisablePlugin = + (delegate* unmanaged[Stdcall]) (delegate* unmanaged[Stdcall]) &RawDisablePlugin; AssertTrueWithRawBits(record.GetVersion(&version, sizeof(PluginVersion)), raw); AssertTrueWithRawBits(record.EnablePlugin(&managedExports, 7u), raw); @@ -153,7 +154,8 @@ private static int RawDisablePlugin() } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] - private static Bool32 SwapMemoryViewAddresses(nuint* disassemblerAddress, nuint* selectedAddress, nuint* hexViewAddress) + private static Bool32 SwapMemoryViewAddresses(nuint* disassemblerAddress, nuint* selectedAddress, + nuint* hexViewAddress) { _ = selectedAddress; (*disassemblerAddress, *hexViewAddress) = (*hexViewAddress, *disassemblerAddress); diff --git a/tests/CheatEngine.SDK.Abi.Tests/AssemblyConformanceTests.cs b/tests/CheatEngine.SDK.Abi.Tests/AssemblyConformanceTests.cs index fbabec3c..27ec70e6 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/AssemblyConformanceTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/AssemblyConformanceTests.cs @@ -75,7 +75,8 @@ public void Assembly_references_no_other_cheatengine_sdk_assembly() [Fact] public void Every_structure_is_listed_in_the_expected_size_table() { - string[] actual = AbiStructures.All().Select(static type => type.FullName!).Order(StringComparer.Ordinal).ToArray(); + string[] actual = AbiStructures.All().Select(static type => type.FullName!).Order(StringComparer.Ordinal) + .ToArray(); string[] expected = ExpectedSizesOn64Bit.Keys.Order(StringComparer.Ordinal).ToArray(); Assert.Equal(expected, actual); @@ -100,7 +101,8 @@ public void Every_structure_including_internal_and_nested_ones_has_the_expected_ public void Public_structures_are_a_subset_of_the_gated_structures() { Assert.All(PublicStructures(), - static type => Assert.True(ExpectedSizesOn64Bit.ContainsKey(type.FullName!), $"{type.FullName} is not gated.")); + static type => Assert.True(ExpectedSizesOn64Bit.ContainsKey(type.FullName!), + $"{type.FullName} is not gated.")); } [Fact] diff --git a/tests/CheatEngine.SDK.Abi.Tests/FieldLayout/FieldLayoutContractTests.cs b/tests/CheatEngine.SDK.Abi.Tests/FieldLayout/FieldLayoutContractTests.cs index a0646729..d34418e6 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/FieldLayout/FieldLayoutContractTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/FieldLayout/FieldLayoutContractTests.cs @@ -48,7 +48,8 @@ public void Every_instance_field_of_every_abi_structure_has_a_layout_row() [Fact] public void Every_layout_row_names_an_existing_field() { - Dictionary structures = AbiStructures.All().ToDictionary(static type => type.FullName!, StringComparer.Ordinal); + Dictionary structures = + AbiStructures.All().ToDictionary(static type => type.FullName!, StringComparer.Ordinal); List stale = []; HashSet<(string, string)> seen = []; @@ -145,7 +146,8 @@ public void Reflected_offsets_agree_with_address_of_offsets_for_the_packed_init_ PluginInitRecord init = default; AssertSameOffset(nameof(PluginInitRecord.Name), Layout.OffsetOf(&init, &init.Name)); - AssertSameOffset(nameof(PluginInitRecord.GetVersion), Layout.OffsetOf(&init, &init.GetVersion)); + AssertSameOffset(nameof(PluginInitRecord.GetVersion), + Layout.OffsetOf(&init, &init.GetVersion)); AssertSameOffset(nameof(PluginInitRecord.EnablePlugin), Layout.OffsetOf(&init, &init.EnablePlugin)); AssertSameOffset(nameof(PluginInitRecord.DisablePlugin), @@ -169,12 +171,14 @@ public void Reflected_offsets_agree_with_address_of_offsets_for_the_packed_init_ PluginType0Record record = default; AssertSameOffset(nameof(PluginType0Record.InterpretedAddress), Layout.OffsetOf(&record, &record.InterpretedAddress)); - AssertSameOffset(nameof(PluginType0Record.Address), Layout.OffsetOf(&record, &record.Address)); + AssertSameOffset(nameof(PluginType0Record.Address), + Layout.OffsetOf(&record, &record.Address)); AssertSameOffset(nameof(PluginType0Record.IsPointer), Layout.OffsetOf(&record, &record.IsPointer)); AssertSameOffset(nameof(PluginType0Record.CountOffsets), Layout.OffsetOf(&record, &record.CountOffsets)); - AssertSameOffset(nameof(PluginType0Record.Offsets), Layout.OffsetOf(&record, &record.Offsets)); + AssertSameOffset(nameof(PluginType0Record.Offsets), + Layout.OffsetOf(&record, &record.Offsets)); AssertSameOffset(nameof(PluginType0Record.Description), Layout.OffsetOf(&record, &record.Description)); AssertSameOffset(nameof(PluginType0Record.ValueType), @@ -204,7 +208,8 @@ public void Gate_reports_a_missing_row_an_extra_row_a_wrong_offset_and_a_wrong_w Assert.Empty(FieldLayoutGate.FindViolations(typeof(Probe), [first, second, third])); Assert.Contains(FieldLayoutGate.FindViolations(typeof(Probe), [first, second]), - static violation => violation.EndsWith("Probe.Third: missing layout row for this field.", StringComparison.Ordinal)); + static violation => violation.EndsWith("Probe.Third: missing layout row for this field.", + StringComparison.Ordinal)); Assert.Contains(FieldLayoutGate.FindViolations(typeof(Probe), [first, second, third, new FieldLayoutRow(probe, "Fourth", 24, 4, FieldKind.Integer)]), static violation => violation.EndsWith("Probe.Fourth: extra layout row names no field of the structure.", @@ -213,8 +218,10 @@ public void Gate_reports_a_missing_row_an_extra_row_a_wrong_offset_and_a_wrong_w static violation => violation.EndsWith("Probe.Second: offset 8, expected 4.", StringComparison.Ordinal)); Assert.Contains(FieldLayoutGate.FindViolations(typeof(Probe), [first, second with { Width = 4 }, third]), static violation => violation.EndsWith("Probe.Second: width 8, expected 4.", StringComparison.Ordinal)); - Assert.Contains(FieldLayoutGate.FindViolations(typeof(Probe), [first, second, third with { Kind = FieldKind.Pointer }]), - static violation => violation.EndsWith("Probe.Third: kind OpaquePointer, expected Pointer.", StringComparison.Ordinal)); + Assert.Contains( + FieldLayoutGate.FindViolations(typeof(Probe), [first, second, third with { Kind = FieldKind.Pointer }]), + static violation => violation.EndsWith("Probe.Third: kind OpaquePointer, expected Pointer.", + StringComparison.Ordinal)); Assert.Contains(FieldLayoutGate.FindViolations(typeof(Probe), [first, first, second, third]), static violation => violation.EndsWith("Probe.First: duplicate layout row.", StringComparison.Ordinal)); } @@ -233,7 +240,8 @@ public void Gate_catches_a_size_preserving_retype_that_a_size_check_misses() Assert.Equal(Layout.SizeOf(), Layout.SizeOf()); IReadOnlyList violations = FieldLayoutGate.FindViolations(typeof(ShiftedMirror), hostRows); - Assert.Contains(violations, static violation => violation.Contains("ShiftedMirror.Second: offset 4, expected 8.", + Assert.Contains(violations, static violation => violation.Contains( + "ShiftedMirror.Second: offset 4, expected 8.", StringComparison.Ordinal)); Assert.Contains(violations, static violation => violation.Contains("ShiftedMirror.Second: width 4, expected 8.", StringComparison.Ordinal)); diff --git a/tests/CheatEngine.SDK.Abi.Tests/Managed/ManagedExportedFunctionsTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Managed/ManagedExportedFunctionsTests.cs index 7d299129..bb76a09b 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Managed/ManagedExportedFunctionsTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Managed/ManagedExportedFunctionsTests.cs @@ -50,7 +50,7 @@ public void Overlay_on_a_raw_host_record_on_64_bit_reads_every_slot() Unsafe.WriteUnaligned(raw, 48); for (int slot = 1; slot <= 5; slot++) { - Unsafe.WriteUnaligned(raw + slot * 8, (ulong) slot * 0x1000_0000_1000UL); + Unsafe.WriteUnaligned(raw + (slot * 8), (ulong) slot * 0x1000_0000_1000UL); } ManagedExportedFunctions copy = *(ManagedExportedFunctions*) raw; diff --git a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicDebugEventDispatcherTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicDebugEventDispatcherTests.cs index 109796ba..b7429f3f 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicDebugEventDispatcherTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicDebugEventDispatcherTests.cs @@ -20,7 +20,8 @@ namespace CheatEngine.SDK.Abi.Tests.Native; /// The Q38 tests call the registered thunk from threads created with Win32 CreateThread, which the CLR did /// not create and has never seen before the call (the situation of a debug event delivered by a host thread). They /// are C1 evidence of the dispatcher's thread contract only: on the managed-hostfxr profile no route reaches the -/// classic RegisterFunction, so Q38 at C3 stays NotApplicable (see libs/CheatEngine.SDK.Abi/README.md). +/// classic RegisterFunction, so Q38 at C3 stays NotApplicable (see +/// libs/CheatEngine.SDK.Abi/README.md). /// /// public sealed unsafe partial class ClassicDebugEventDispatcherTests : IDisposable @@ -484,7 +485,8 @@ public void Function_id_zero_is_a_valid_identifier_passed_back_on_unregister() [InlineData(true, false)] [InlineData(false, true)] [InlineData(false, false)] - public void Missing_register_or_unregister_slot_is_refused_before_any_host_call(bool withRegister, bool withUnregister) + public void Missing_register_or_unregister_slot_is_refused_before_any_host_call(bool withRegister, + bool withUnregister) { Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); s_registerCalls = 0; diff --git a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicExportedFunctionsSlotReaderTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicExportedFunctionsSlotReaderTests.cs index 70bd6b40..57b4f7b4 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicExportedFunctionsSlotReaderTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicExportedFunctionsSlotReaderTests.cs @@ -37,7 +37,8 @@ public void Slot_is_refused_when_the_declared_size_is_one_byte_short(int slot, i byte[] table = Table(minDeclaredSize - 1, ClassicExportedFunctionsSlotReader.TableByteCount); WriteSlot(table, slot, 0x1234_5678_9ABC_DEF0); - Assert.False(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.False( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); Assert.Equal(default, observation); } @@ -52,7 +53,8 @@ public void Slot_is_read_when_declared_and_physical_sizes_cover_it_exactly(int s ulong value = 0x0100_0000_0000_0000UL + (ulong) slot; WriteSlot(table, slot, value); - Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.True( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); Assert.Equal(slot, observation.Slot); Assert.Equal((nint) value, observation.RawValue); Assert.False(observation.IsNull); @@ -74,23 +76,27 @@ public void Oversized_declared_table_is_accepted_for_the_known_slots(int declare for (int slot = 1; slot < ClassicExportedFunctionsSlotReader.SlotCount; slot++) { - Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); - Assert.Equal((nint) (slot * 0x10), observation.RawValue); + Assert.True( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.Equal(slot * 0x10, observation.RawValue); } // A future suffix is compatible with the known slots, but no slot past the known table is ever read. - Assert.False(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ClassicExportedFunctionsSlotReader.SlotCount, out _)); + Assert.False(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ClassicExportedFunctionsSlotReader.SlotCount, + out _)); } [Theory] [MemberData(nameof(RegistrySlots))] [Trait("Qualification", "Q39")] - public void Physically_truncated_table_is_refused_even_when_the_declared_size_is_large(int slot, int minDeclaredSize) + public void Physically_truncated_table_is_refused_even_when_the_declared_size_is_large(int slot, + int minDeclaredSize) { Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, minDeclaredSize - 1); - Assert.False(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.False( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); Assert.Equal(default, observation); } @@ -99,17 +105,23 @@ public void Physically_truncated_table_is_refused_even_when_the_declared_size_is public void Null_slot_is_reported_as_null_and_never_dereferenced() { Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); - byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, ClassicExportedFunctionsSlotReader.TableByteCount); - int[] nilSlots = [.. ClassicSlotRegistry.Slots - .Where(static slot => string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", StringComparison.Ordinal)) - .Select(static slot => slot.GetProperty("slot").GetInt32())]; + byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, + ClassicExportedFunctionsSlotReader.TableByteCount); + int[] nilSlots = + [ + .. ClassicSlotRegistry.Slots + .Where(static slot => string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", + StringComparison.Ordinal)) + .Select(static slot => slot.GetProperty("slot").GetInt32()) + ]; // A non-nil but invalid address in a neighbouring slot: reading it must not touch the address either. WriteSlot(table, 15, 0x1); Assert.Equal(11, nilSlots.Length); foreach (int slot in nilSlots) { - Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.True( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); Assert.True(observation.IsNull); Assert.Equal(0, observation.RawValue); } @@ -127,12 +139,15 @@ public void Cell_contents_changed_between_two_reads_are_two_distinct_observation const int ReadProcessMemorySlot = 18; Assert.Equal("FunctionPointerCell", ClassicSlotRegistry.Slots[ReadProcessMemorySlot].GetProperty("nature").GetString()); - byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, ClassicExportedFunctionsSlotReader.TableByteCount); + byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, + ClassicExportedFunctionsSlotReader.TableByteCount); WriteSlot(table, ReadProcessMemorySlot, 0x0000_7FF0_0000_1000); - Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ReadProcessMemorySlot, out ClassicSlotObservation first)); + Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ReadProcessMemorySlot, + out ClassicSlotObservation first)); WriteSlot(table, ReadProcessMemorySlot, 0x0000_7FF0_0000_2000); - Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ReadProcessMemorySlot, out ClassicSlotObservation second)); + Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, ReadProcessMemorySlot, + out ClassicSlotObservation second)); Assert.Equal(0x0000_7FF0_0000_1000, first.RawValue); Assert.Equal(0x0000_7FF0_0000_2000, second.RawValue); @@ -144,7 +159,8 @@ public void Cell_contents_changed_between_two_reads_are_two_distinct_observation public void Address_above_32_bits_round_trips_through_a_slot() { Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); - byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, ClassicExportedFunctionsSlotReader.TableByteCount); + byte[] table = Table(ClassicExportedFunctionsSlotReader.TableByteCount, + ClassicExportedFunctionsSlotReader.TableByteCount); WriteSlot(table, 158, HighAddress); Assert.True(ClassicExportedFunctionsSlotReader.TryReadSlot(table, 158, out ClassicSlotObservation observation)); @@ -163,7 +179,8 @@ public void Slot_zero_and_out_of_range_indices_are_refused(int slot) { byte[] table = Table(int.MaxValue, ClassicExportedFunctionsSlotReader.TableByteCount + 64); - Assert.False(ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); + Assert.False( + ClassicExportedFunctionsSlotReader.TryReadSlot(table, slot, out ClassicSlotObservation observation)); Assert.Equal(default, observation); } @@ -173,7 +190,8 @@ public void Reader_constants_equal_the_registry_contract() JsonElement contract = ClassicSlotRegistry.Root.GetProperty("contract"); Assert.Equal(ClassicExportedFunctionsSlotReader.SlotCount, contract.GetProperty("fieldCount").GetInt32()); - Assert.Equal(ClassicExportedFunctionsSlotReader.TableByteCount, contract.GetProperty("x64TableSize").GetInt32()); + Assert.Equal(ClassicExportedFunctionsSlotReader.TableByteCount, + contract.GetProperty("x64TableSize").GetInt32()); Assert.Equal(ClassicExportedFunctionsSlotReader.TableByteCount, ClassicSlotRegistry.MinDeclaredSize(158)); } diff --git a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicSlotRegistryPrefixTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicSlotRegistryPrefixTests.cs index 3879d9cf..6579726e 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicSlotRegistryPrefixTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Native/ClassicSlotRegistryPrefixTests.cs @@ -34,7 +34,8 @@ public void Prefix_fields_are_registry_slots_0_to_17_in_order_with_offsets_and_w Assert.Equal(entry.GetProperty("width").GetInt32(), FieldLayoutGate.WidthOf(field)); } - Assert.All(slots[18..], static entry => Assert.Equal(JsonValueKind.Null, entry.GetProperty("sdkField").ValueKind)); + Assert.All(slots[18..], + static entry => Assert.Equal(JsonValueKind.Null, entry.GetProperty("sdkField").ValueKind)); } [Fact] @@ -42,11 +43,13 @@ public void Direct_prefix_byte_count_is_the_min_declared_size_of_slot_17() { Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); - Assert.Equal(ClassicExportedFunctionsPrefixReader.DirectPrefixByteCount, ClassicSlotRegistry.MinDeclaredSize(17)); + Assert.Equal(ClassicExportedFunctionsPrefixReader.DirectPrefixByteCount, + ClassicSlotRegistry.MinDeclaredSize(17)); Assert.Equal(ClassicSlotRegistry.MinDeclaredSize(17), Layout.SizeOf()); Assert.Equal(ClassicExportedFunctionsPrefixReader.DirectPrefixByteCount, ClassicSlotRegistry.Root.GetProperty("contract").GetProperty("sdkDirectPrefixSize").GetInt32()); - Assert.True(ClassicExportedFunctionsPrefixReader.DirectPrefixByteCount < ClassicSlotRegistry.MinDeclaredSize(18)); + Assert.True( + ClassicExportedFunctionsPrefixReader.DirectPrefixByteCount < ClassicSlotRegistry.MinDeclaredSize(18)); } [Fact] @@ -58,7 +61,8 @@ public void Typed_prefix_slots_are_exactly_the_registry_prefix_typed_slots() { bool opaque = FieldLayoutGate.KindOf(PrefixFields[slot]) == FieldKind.OpaquePointer; string expected = opaque ? "PrefixOpaque" : "PrefixTyped"; - Assert.True(string.Equals(expected, slots[slot].GetProperty("sdkExposure").GetString(), StringComparison.Ordinal), + Assert.True( + string.Equals(expected, slots[slot].GetProperty("sdkExposure").GetString(), StringComparison.Ordinal), $"Slot {slot} ({PrefixFields[slot].Name}) is {expected} in the SDK but {slots[slot].GetProperty("sdkExposure").GetString()} in the registry."); } } @@ -72,7 +76,8 @@ public void Nil_or_divergent_prefix_slots_are_opaque_void_pointers() for (int slot = 0; slot < PrefixFields.Length; slot++) { JsonElement entry = slots[slot]; - bool nil = string.Equals(entry.GetProperty("nullability").GetString(), "NilAssigned", StringComparison.Ordinal); + bool nil = string.Equals(entry.GetProperty("nullability").GetString(), "NilAssigned", + StringComparison.Ordinal); bool divergent = entry.GetProperty("divergenceRefs").GetArrayLength() > 0; if (nil || divergent) { diff --git a/tests/CheatEngine.SDK.Abi.Tests/Native/ExportedFunctionsPrefixTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Native/ExportedFunctionsPrefixTests.cs index 6d800d08..4bda49d2 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Native/ExportedFunctionsPrefixTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Native/ExportedFunctionsPrefixTests.cs @@ -81,7 +81,7 @@ public void RegisterFunction_and_ChangeRegistersAtAddress_accept_the_declared_st [Fact] public void Prefix_stops_before_the_pointer_to_pointer_hook_suffix() { - FieldInfo[] fields = typeof(ExportedFunctionsPrefix).GetFields(BindingFlags.Instance | BindingFlags.Public); + _ = typeof(ExportedFunctionsPrefix).GetFields(BindingFlags.Instance | BindingFlags.Public); Assert.Null(typeof(ExportedFunctionsPrefix).GetField("ReadProcessMemory", BindingFlags.Instance | BindingFlags.Public)); diff --git a/tests/CheatEngine.SDK.Abi.Tests/Native/SelectedRecordOracleTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Native/SelectedRecordOracleTests.cs index 749e82ee..e78032da 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Native/SelectedRecordOracleTests.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Native/SelectedRecordOracleTests.cs @@ -17,7 +17,8 @@ namespace CheatEngine.SDK.Abi.Tests.Native; /// per-field check separates them (audit annex 05, A03-31, AX05-07). /// /// -/// The mirrors are modelled here as nested C# structures, never in CheatEngine.SDK.Abi. The CI-only fixture test +/// The mirrors are modelled here as nested C# structures, never in CheatEngine.SDK.Abi. The CI-only fixture +/// test /// compares the same three layouts as compiled by MSVC from the transcriptions in /// tests/native-abi-fixture/ce77_plugin_abi_contract.h. Nothing here observes the CE 7.7 binary. /// @@ -44,7 +45,8 @@ public void PluginType0Record_matches_the_pinned_host_type_field_by_field() Assert.SkipUnless(Layout.Is64BitProcess, Layout.Requires64BitProcess); Assert.Equal(48, Layout.SizeOf()); - Assert.Empty(FieldLayoutGate.FindViolations(typeof(PluginType0Record), HostRows(HostType, FieldKindsOfSdkRecord()))); + Assert.Empty(FieldLayoutGate.FindViolations(typeof(PluginType0Record), + HostRows(HostType, FieldKindsOfSdkRecord()))); Assert.Equal(HostLayout.Select(static row => row.Field), typeof(PluginType0Record).GetFields(FieldLayoutGate.InstanceFields).Select(static field => field.Name), StringComparer.Ordinal); @@ -93,7 +95,8 @@ public void Size_only_check_cannot_tell_the_host_type_from_either_mirror() Assert.Equal(Layout.SizeOf(), Layout.SizeOf()); // What the per-field gate sees against the host oracle: the SDK record passes, both mirrors fail. - Assert.Empty(FieldLayoutGate.FindViolations(typeof(PluginType0Record), HostRows(HostType, FieldKindsOfSdkRecord()))); + Assert.Empty(FieldLayoutGate.FindViolations(typeof(PluginType0Record), + HostRows(HostType, FieldKindsOfSdkRecord()))); Assert.Contains(FieldLayoutGate.FindViolations(typeof(PascalDwordMirrorSelectedRecord), HostRows(typeof(PascalDwordMirrorSelectedRecord).FullName!, null)), IsOffsetOrWidth); Assert.Contains(FieldLayoutGate.FindViolations(typeof(PascalBooleanMirrorSelectedRecord), diff --git a/tests/CheatEngine.SDK.Abi.Tests/README.md b/tests/CheatEngine.SDK.Abi.Tests/README.md index 0f0c4f75..fc37ae2d 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/README.md +++ b/tests/CheatEngine.SDK.Abi.Tests/README.md @@ -19,15 +19,16 @@ Engine itself. Independent techniques check the layout, so one mistake cannot hide behind another. `Managed/` and `Native/` mirror the `CheatEngine.SDK.Abi` namespaces. `Support/` holds `Layout`, the `AbiShape` gate, the per-field `FieldLayoutGate` -with its literal `FieldLayoutExpectations` table, and their tests. Expected numbers are literals next to the assertion or +with its literal `FieldLayoutExpectations` table, and their tests. Expected numbers are literals next to the assertion +or in that table, never derived from the code under test. -| Technique | What it does | -|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Address-of arithmetic | `Layout.SizeOf()` and `Layout.OffsetOf` measure the size and every field offset. | -| Raw bytes | The packed 36-byte init record is written into a guard-filled buffer at an aligned and an odd address. The 48-byte exports record is built as raw bytes, then read through the struct. | -| Host simulation | `&Method` of a real `[UnmanagedCallersOnly]` `Stdcall` function is stored in every typed function-pointer slot, then called through the field. | -| Per-field gate | `FieldLayout/FieldLayoutContractTests` walks every instance field of every struct (public, internal, nested private, compiler-generated backing fields) and compares its offset (measured with the IL `ldflda` instruction), width and kind with one literal row of `Support/FieldLayoutExpectations.cs`, in both directions. | +| Technique | What it does | +|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Address-of arithmetic | `Layout.SizeOf()` and `Layout.OffsetOf` measure the size and every field offset. | +| Raw bytes | The packed 36-byte init record is written into a guard-filled buffer at an aligned and an odd address. The 48-byte exports record is built as raw bytes, then read through the struct. | +| Host simulation | `&Method` of a real `[UnmanagedCallersOnly]` `Stdcall` function is stored in every typed function-pointer slot, then called through the field. | +| Per-field gate | `FieldLayout/FieldLayoutContractTests` walks every instance field of every struct (public, internal, nested private, compiler-generated backing fields) and compares its offset (measured with the IL `ldflda` instruction), width and kind with one literal row of `Support/FieldLayoutExpectations.cs`, in both directions. | | Native-fact comparison | The native CI job builds the checked `ce77-native-abi-facts.txt`; the Debug build-test job passes it in and sets the required gate. A compiled managed test measures every fixture-covered layout (C-header records and, since schema 3, the packed managed bootstrap record and the managed exports) and compares its size, alignment, and every field's offset and width to that output. | The test assembly applies `[assembly: DisableRuntimeMarshalling]`, so calls take the path a plugin takes. @@ -36,14 +37,17 @@ for `Bool8`). Every branch of `AbiArchitecture` is tested through its internal o `NativeAbiFixtureManagedComparisonTests` has no local fixture dependency. The Debug build-test CI job downloads the facts the native job built, supplies `CE77_NATIVE_ABI_FACTS_PATH` and sets `CE77_NATIVE_ABI_REQUIRED=true` for its -solution test run, which makes a missing facts path fail the comparison gate. Ordinary managed runs omit the required mode and can omit the facts path; that local opt-out is +solution test run, which makes a missing facts path fail the comparison gate. Ordinary managed runs omit the required +mode and can omit the facts path; that local opt-out is intentional and is not a substitute for an exact-host test. - `AssemblyConformanceTests` compares an expected-size table with every struct of the assembly (public, internal and nested, keyed by full name) in both directions, then runs the `AbiShape` reflection gate over every struct. - `FieldLayoutContractTests` is the per-field gate: a field without a row, a row without a field, or a wrong offset, - width or kind fails the run. `Reflected_offsets_agree_with_address_of_offsets_for_the_packed_init_record_and_the_managed_exports` - proves that the reflected offsets equal the C# address-of arithmetic, and two wrong-on-purpose nested structs prove the + width or kind fails the run. + `Reflected_offsets_agree_with_address_of_offsets_for_the_packed_init_record_and_the_managed_exports` + proves that the reflected offsets equal the C# address-of arithmetic, and two wrong-on-purpose nested structs prove + the gate reports each class of error (including a size-preserving retype that a size check misses). - The gate rejects `bool`, `char`, references, foreign value types, managed or non-`Stdcall` function pointers and by-reference parameters at any depth. It exists because `delegate* unmanaged[Cdecl]` compiles without a @@ -65,7 +69,8 @@ intentional and is not a substitute for an exact-host test. An unpacked mirror is 40 bytes and writes 4 bytes past the host's 36-byte variable. - The type-0 selection record matches the host type of `plugin.pas` field by field, and both same-size Pascal mirrors are told apart by a per-field check (`SelectedRecordOracleTests`). -- The embedded [classic slot registry](../CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json) agrees with `ExportedFunctionsPrefix` +- The embedded [classic slot registry](../CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json) + agrees with `ExportedFunctionsPrefix` on slots 0-17, their offsets, widths and opacity (`ClassicSlotRegistryPrefixTests`). - A classic slot is observed only when the declared and the physical sizes both reach `8 * (slot + 1)` bytes, for every slot and boundary, and the prefix copy is all or nothing between slot boundaries diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/AbiStructures.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/AbiStructures.cs index ae1aa571..a76076b2 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/AbiStructures.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/AbiStructures.cs @@ -8,7 +8,8 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// /// The set of structures the assembly-wide gates inspect: every value type of CheatEngine.SDK.Abi, public, /// internal or nested private alike, minus enumerations and compiler-generated types. Shared by -/// (sizes and shape) and FieldLayout.FieldLayoutContractTests (per-field +/// (sizes and shape) and FieldLayout.FieldLayoutContractTests +/// (per-field /// offsets and widths), so both gates always look at exactly the same structures. /// internal static class AbiStructures diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/ClassicSlotRegistry.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/ClassicSlotRegistry.cs index c591e2d8..4d727552 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/ClassicSlotRegistry.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/ClassicSlotRegistry.cs @@ -3,7 +3,8 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// -/// Read access to the committed tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json, embedded in this test assembly. The +/// Read access to the committed tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json, +/// embedded in this test assembly. The /// registry's authority is the pinned host source plugin.pas; the SDK types must agree with it, never the /// other way round. /// @@ -31,7 +32,8 @@ public static int MinDeclaredSize(int slot) private static JsonElement Load() { using Stream stream = typeof(ClassicSlotRegistry).Assembly.GetManifestResourceStream(ResourceName) - ?? throw new InvalidOperationException($"The embedded resource {ResourceName} is missing."); + ?? throw new InvalidOperationException( + $"The embedded resource {ResourceName} is missing."); using JsonDocument document = JsonDocument.Parse(stream); return document.RootElement.Clone(); } diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldKind.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldKind.cs index 0e12d9e5..dbe0e0f3 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldKind.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldKind.cs @@ -3,7 +3,10 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// What a field of an ABI structure is, as far as the layout gate is concerned. internal enum FieldKind { - /// A fixed-width or pointer-sized integer (int, uint, long, byte, nuint, an enumeration). + /// + /// A fixed-width or pointer-sized integer (int, uint, long, byte, nuint, an + /// enumeration). + /// Integer, /// One of the two ABI booleans, or . diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutExpectations.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutExpectations.cs index 22dcafc3..f785933f 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutExpectations.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutExpectations.cs @@ -8,7 +8,8 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// /// /// Literals only, never derived from the code under test. Adding, removing, moving or retyping a field without -/// editing this table fails FieldLayout.FieldLayoutContractTests in both directions (a field without a row, a row +/// editing this table fails FieldLayout.FieldLayoutContractTests in both directions (a field without a row, +/// a row /// without a field, a wrong offset, width or kind). /// /// @@ -16,7 +17,8 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// TPluginDotNetInitResult L29-36 packed, TExportedFunctionsDotNetV1 L38-45, /// TPlugin0_SelectedRecord L726-735, TExportedFunctions5 L47-226), the C header /// cepluginsdk.h for the registration records and REGISTERMODIFICATIONINFO, and the x64 natural -/// alignment rule; see libs/CheatEngine.SDK.Abi/README.md. The PluginType0Record rows are identical to the host +/// alignment rule; see libs/CheatEngine.SDK.Abi/README.md. The PluginType0Record rows are identical +/// to the host /// type table of the audit's annex 05 (the oracle of Native.SelectedRecordOracleTests). /// /// diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutGate.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutGate.cs index 35e83986..9337c54d 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutGate.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutGate.cs @@ -14,7 +14,8 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// /// Offset is measured, not computed: a applies the IL instruction /// ldflda for the field to the address of a zeroed native buffer of the structure's size and subtracts the -/// buffer address. That is exactly the arithmetic the JIT performs for &record->Field in a plugin, it +/// buffer address. That is exactly the arithmetic the JIT performs for &record->Field in a plugin, +/// it /// works for private and compiler-generated fields, and it does not go through /// , whose unmanaged view is what CA1421 warns about in an /// assembly with runtime marshalling disabled. The field is never read or written. @@ -93,7 +94,8 @@ public static IReadOnlyList FindViolations(Type structure, IReadOnlyList public static unsafe int OffsetOf(FieldInfo field) { ArgumentNullException.ThrowIfNull(field); - Type declaringType = field.DeclaringType ?? throw new ArgumentException("A field has a declaring type.", nameof(field)); + Type declaringType = field.DeclaringType ?? + throw new ArgumentException("A field has a declaring type.", nameof(field)); if (!declaringType.IsValueType || field.IsStatic) { throw new ArgumentException("Only instance fields of structures have a layout offset.", nameof(field)); diff --git a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutRow.cs b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutRow.cs index 7715b140..91e7f73d 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutRow.cs +++ b/tests/CheatEngine.SDK.Abi.Tests/Support/FieldLayoutRow.cs @@ -6,4 +6,9 @@ namespace CheatEngine.SDK.Abi.Tests.Support; /// Byte offset of the field on x64. /// Byte width of the field on x64 (a pointer or function pointer is 8). /// What the field is; a retyped void* keeps its width but changes its kind. -internal readonly record struct FieldLayoutRow(string TypeFullName, string FieldName, int Offset, int Width, FieldKind Kind); +internal readonly record struct FieldLayoutRow( + string TypeFullName, + string FieldName, + int Offset, + int Width, + FieldKind Kind); diff --git a/tests/CheatEngine.SDK.Abi.Tests/packages.lock.json b/tests/CheatEngine.SDK.Abi.Tests/packages.lock.json index 0729acf9..bc0d6e39 100644 --- a/tests/CheatEngine.SDK.Abi.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Abi.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaBindingAnalyzerTests.cs b/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaBindingAnalyzerTests.cs index fbb07768..2a3e8384 100644 --- a/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaBindingAnalyzerTests.cs +++ b/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaBindingAnalyzerTests.cs @@ -39,6 +39,7 @@ public sealed class LuaBindingAnalyzerTests private const string OptionalUsings = "using System;\nusing CheatEngine.SDK.Annotations.Lua;\nusing CheatEngine.SDK.Lua.Calls;\nusing CheatEngine.SDK.Lua.Marshalling;\nnamespace Demo;\n"; + private static readonly CSharpParseOptions ParseOptions = new(LanguageVersion.CSharp14); // The real SDK assemblies the shape checks are written against, taken from the copies loaded in this test @@ -562,24 +563,35 @@ public static partial class Bindings """, true); Diagnostic[] reported = - [.. diagnostics.Where(static d => string.Equals(d.Id, DiagnosticIds.NonTrailingOptionalLuaArgument, StringComparison.Ordinal))]; + [ + .. diagnostics.Where(static d => + string.Equals(d.Id, DiagnosticIds.NonTrailingOptionalLuaArgument, StringComparison.Ordinal)) + ]; Assert.Equal(2, reported.Length); - Assert.Contains(reported, static d => d.GetMessage(CultureInfo.InvariantCulture).StartsWith("Lua binding 'G' must declare every LuaOptional argument after the required ones", StringComparison.Ordinal)); - Assert.Contains(reported, static d => d.GetMessage(CultureInfo.InvariantCulture).StartsWith("Lua binding 'F' must declare every LuaOptional parameter after the required ones", StringComparison.Ordinal)); - Assert.DoesNotContain(diagnostics, static d => string.Equals(d.Id, DiagnosticIds.InvalidLuaGlobal, StringComparison.Ordinal) - || string.Equals(d.Id, DiagnosticIds.InvalidLuaFunction, StringComparison.Ordinal)); + Assert.Contains(reported, + static d => d.GetMessage(CultureInfo.InvariantCulture).StartsWith( + "Lua binding 'G' must declare every LuaOptional argument after the required ones", + StringComparison.Ordinal)); + Assert.Contains(reported, + static d => d.GetMessage(CultureInfo.InvariantCulture).StartsWith( + "Lua binding 'F' must declare every LuaOptional parameter after the required ones", + StringComparison.Ordinal)); + Assert.DoesNotContain(diagnostics, static d => + string.Equals(d.Id, DiagnosticIds.InvalidLuaGlobal, StringComparison.Ordinal) + || string.Equals(d.Id, DiagnosticIds.InvalidLuaFunction, StringComparison.Ordinal)); } [Fact] public async Task Required_result_after_an_optional_result_reports_CESDK2011() { ImmutableArray diagnostics = await AnalyzeAsync(OptionalUsings + - "public static partial class Bindings { [LuaGlobal(\"g\")] public static partial bool TryG(out LuaOptional a, out int b); }", + "public static partial class Bindings { [LuaGlobal(\"g\")] public static partial bool TryG(out LuaOptional a, out int b); }", true); Diagnostic diagnostic = Assert.Single(diagnostics); Assert.Equal(DiagnosticIds.InvalidOptionalOrVariadicLuaResult, diagnostic.Id); - Assert.Contains("after the required results", diagnostic.GetMessage(CultureInfo.InvariantCulture), StringComparison.Ordinal); + Assert.Contains("after the required results", diagnostic.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal); } [Fact] @@ -602,13 +614,21 @@ public static partial class Bindings string[] messages = [ .. diagnostics - .Where(static d => string.Equals(d.Id, DiagnosticIds.InvalidOptionalOrVariadicLuaResult, StringComparison.Ordinal)) + .Where(static d => string.Equals(d.Id, DiagnosticIds.InvalidOptionalOrVariadicLuaResult, + StringComparison.Ordinal)) .Select(static d => d.GetMessage(CultureInfo.InvariantCulture)) ]; - Assert.Contains(messages, static m => m.StartsWith("Lua global binding 'TryG' must return LuaOperationStatus", StringComparison.Ordinal)); - Assert.Contains(messages, static m => m.StartsWith("Lua global binding 'H' must declare at most one variadic", StringComparison.Ordinal)); - Assert.Contains(messages, static m => m.StartsWith("Lua global binding 'H' must declare the variadic", StringComparison.Ordinal)); - Assert.Contains(messages, static m => m.StartsWith("Lua global binding 'K' must use int, long, float, double, bool or nuint", StringComparison.Ordinal)); + Assert.Contains(messages, + static m => m.StartsWith("Lua global binding 'TryG' must return LuaOperationStatus", + StringComparison.Ordinal)); + Assert.Contains(messages, + static m => m.StartsWith("Lua global binding 'H' must declare at most one variadic", + StringComparison.Ordinal)); + Assert.Contains(messages, + static m => m.StartsWith("Lua global binding 'H' must declare the variadic", StringComparison.Ordinal)); + Assert.Contains(messages, + static m => m.StartsWith("Lua global binding 'K' must use int, long, float, double, bool or nuint", + StringComparison.Ordinal)); } [Fact] @@ -644,14 +664,17 @@ public static partial class Bindings Diagnostic[] reported = [ .. (await GetDiagnosticsAsync(compilation)) - .Where(static d => string.Equals(d.Id, DiagnosticIds.LookAlikeLuaContractType, StringComparison.Ordinal)) + .Where(static d => string.Equals(d.Id, DiagnosticIds.LookAlikeLuaContractType, StringComparison.Ordinal)) ]; Assert.Equal(2, reported.Length); - Assert.All(reported, static d => Assert.Contains("not a same-named type", d.GetMessage(CultureInfo.InvariantCulture), StringComparison.Ordinal)); + Assert.All(reported, + static d => Assert.Contains("not a same-named type", d.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal)); } [Fact] - public async Task Same_named_LuaOperationStatus_from_source_reports_CESDK2012_instead_of_selecting_the_outcome_form() + public async Task + Same_named_LuaOperationStatus_from_source_reports_CESDK2012_instead_of_selecting_the_outcome_form() { const string Source = """ using CheatEngine.SDK.Annotations.Lua; @@ -680,7 +703,8 @@ public static partial class Bindings ImmutableArray diagnostics = await GetDiagnosticsAsync(compilation); Diagnostic diagnostic = Assert.Single(diagnostics); Assert.Equal(DiagnosticIds.LookAlikeLuaContractType, diagnostic.Id); - Assert.StartsWith("Lua binding 'TryReadInt32' must use the LuaOptional and LuaOperationStatus types of CheatEngine.SDK.Lua", + Assert.StartsWith( + "Lua binding 'TryReadInt32' must use the LuaOptional and LuaOperationStatus types of CheatEngine.SDK.Lua", diagnostic.GetMessage(CultureInfo.InvariantCulture), StringComparison.Ordinal); } @@ -702,8 +726,10 @@ public static partial class Bindings """, true); Assert.Equal(["F", "G", "TryH"], - diagnostics.Where(static d => string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)) - .Select(static d => d.GetMessage(CultureInfo.InvariantCulture).Split('\'')[1]).Order(StringComparer.Ordinal), + diagnostics.Where(static d => + string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)) + .Select(static d => d.GetMessage(CultureInfo.InvariantCulture).Split('\'')[1]) + .Order(StringComparer.Ordinal), StringComparer.Ordinal); } @@ -722,10 +748,17 @@ public static partial class Bindings """, true); Diagnostic[] reported = - [.. diagnostics.Where(static d => string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal))]; + [ + .. diagnostics.Where(static d => + string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)) + ]; Assert.Equal(2, reported.Length); - Assert.Contains(reported, static d => d.GetMessage(CultureInfo.InvariantCulture).Contains("the throwing form cannot return one", StringComparison.Ordinal)); - Assert.Contains(reported, static d => d.GetMessage(CultureInfo.InvariantCulture).Contains("a thunk cannot return one", StringComparison.Ordinal)); + Assert.Contains(reported, + static d => d.GetMessage(CultureInfo.InvariantCulture) + .Contains("the throwing form cannot return one", StringComparison.Ordinal)); + Assert.Contains(reported, + static d => d.GetMessage(CultureInfo.InvariantCulture) + .Contains("a thunk cannot return one", StringComparison.Ordinal)); } [Fact] diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaObjectBindingAnalyzerTests.cs b/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaObjectBindingAnalyzerTests.cs index 0d621edf..92adf2fc 100644 --- a/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaObjectBindingAnalyzerTests.cs +++ b/tests/CheatEngine.SDK.Analyzers.Tests/Generation/LuaObjectBindingAnalyzerTests.cs @@ -4,6 +4,9 @@ using CheatEngine.SDK.Analyzers.Diagnostics; using CheatEngine.SDK.Analyzers.Generation; using CheatEngine.SDK.Analyzers.Tests.Infrastructure; +using CheatEngine.SDK.Annotations.Lua; +using CheatEngine.SDK.Lua.Interop.Api; +using CheatEngine.SDK.Lua.State; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -19,9 +22,9 @@ public sealed class LuaObjectBindingAnalyzerTests // The real SDK assemblies: LuaOptional is recognised only as the type CheatEngine.SDK.Lua defines. private static readonly ImmutableArray RealSdkReferences = [ - MetadataReference.CreateFromFile(typeof(CheatEngine.SDK.Annotations.Lua.LuaFunctionAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(CheatEngine.SDK.Lua.Interop.Api.LuaApi).Assembly.Location), - MetadataReference.CreateFromFile(typeof(CheatEngine.SDK.Lua.State.LuaState).Assembly.Location) + MetadataReference.CreateFromFile(typeof(LuaFunctionAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(LuaApi).Assembly.Location), + MetadataReference.CreateFromFile(typeof(LuaState).Assembly.Location) ]; [Fact] @@ -227,14 +230,19 @@ public readonly partial struct Handle .GetAnalyzerDiagnosticsAsync(TestContext.Current.CancellationToken); Assert.Equal(["Load", "TryRead", "Value"], - diagnostics.Where(static d => string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)) - .Select(static d => d.GetMessage(CultureInfo.InvariantCulture).Split('\'')[1]).Order(StringComparer.Ordinal), + diagnostics.Where(static d => + string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)) + .Select(static d => d.GetMessage(CultureInfo.InvariantCulture).Split('\'')[1]) + .Order(StringComparer.Ordinal), StringComparer.Ordinal); - Assert.All(diagnostics.Where(static d => string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)), + Assert.All( + diagnostics.Where(static d => + string.Equals(d.Id, DiagnosticIds.UnsupportedLuaOptionalPosition, StringComparison.Ordinal)), static d => Assert.Contains("[LuaMethod] and [LuaProperty] members do not support optional values yet", d.GetMessage(CultureInfo.InvariantCulture), StringComparison.Ordinal)); // The optional position is explained once, by CESDK2013, not a second time as an unsupported type (CESDK2006). - Assert.DoesNotContain(diagnostics, static d => string.Equals(d.Id, DiagnosticIds.InvalidLuaAnnotationTarget, StringComparison.Ordinal)); + Assert.DoesNotContain(diagnostics, + static d => string.Equals(d.Id, DiagnosticIds.InvalidLuaAnnotationTarget, StringComparison.Ordinal)); } [Fact] diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/Plugin/ClassicNativeExportAnalyzerTests.cs b/tests/CheatEngine.SDK.Analyzers.Tests/Plugin/ClassicNativeExportAnalyzerTests.cs index 7acb26a2..88138741 100644 --- a/tests/CheatEngine.SDK.Analyzers.Tests/Plugin/ClassicNativeExportAnalyzerTests.cs +++ b/tests/CheatEngine.SDK.Analyzers.Tests/Plugin/ClassicNativeExportAnalyzerTests.cs @@ -4,7 +4,6 @@ using CheatEngine.SDK.Analyzers.Tests.Infrastructure; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Testing; using Verifier = CheatEngine.SDK.Analyzers.Tests.Infrastructure.AnalyzerVerifier< CheatEngine.SDK.Analyzers.Plugin.ClassicNativeExportAnalyzer>; @@ -40,22 +39,26 @@ await Verifier.VerifyAsync(Exports($$""" public async Task NativeExportNames_constant_as_entry_point_reports_CESDK0006() { CheatEngineSdkAnalyzerTest test = new(); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(NativeExportNames).Assembly.Location)); + test.TestState.AdditionalReferences.Add( + MetadataReference.CreateFromFile(typeof(NativeExportNames).Assembly.Location)); test.TestState.Sources.Add(("Test0.cs", TestText.Normalize(Exports(""" - [{|#0:UnmanagedCallersOnly(EntryPoint = NativeExportNames.GetVersion)|}] - private static int GetVersion(nint version, int size) => 0; + [{|#0:UnmanagedCallersOnly(EntryPoint = NativeExportNames.GetVersion)|}] + private static int GetVersion(nint version, int size) => 0; - [{|#1:UnmanagedCallersOnly(EntryPoint = NativeExportNames.InitializePlugin)|}] - private static int Initialize(nint exports, int pluginId) => 0; + [{|#1:UnmanagedCallersOnly(EntryPoint = NativeExportNames.InitializePlugin)|}] + private static int Initialize(nint exports, int pluginId) => 0; - [{|#2:UnmanagedCallersOnly(EntryPoint = NativeExportNames.DisablePlugin)|}] - private static int Disable() => 0; - """, "using CheatEngine.SDK.Abi.Native;")))); - test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport).WithLocation(0) + [{|#2:UnmanagedCallersOnly(EntryPoint = NativeExportNames.DisablePlugin)|}] + private static int Disable() => 0; + """, "using CheatEngine.SDK.Abi.Native;")))); + test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport) + .WithLocation(0) .WithArguments("GetVersion", NativeExportNames.GetVersion)); - test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport).WithLocation(1) + test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport) + .WithLocation(1) .WithArguments("Initialize", NativeExportNames.InitializePlugin)); - test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport).WithLocation(2) + test.ExpectedDiagnostics.Add(Verifier.Diagnostic(DiagnosticDescriptors.ClassicNativePluginExport) + .WithLocation(2) .WithArguments("Disable", NativeExportNames.DisablePlugin)); await test.RunAsync(TestContext.Current.CancellationToken); @@ -114,10 +117,12 @@ await Verifier.VerifyWithoutCheatEngineSdkAsync(Exports(""" [Fact] public async Task Export_in_generated_code_is_not_analysed() { - await Verifier.VerifyAsync([("Exports.g.cs", "// \n" + Exports(""" - [UnmanagedCallersOnly(EntryPoint = "CEPlugin_GetVersion")] - private static int Export(nint argument) => 0; - """))]); + await Verifier.VerifyAsync([ + ("Exports.g.cs", "// \n" + Exports(""" + [UnmanagedCallersOnly(EntryPoint = "CEPlugin_GetVersion")] + private static int Export(nint argument) => 0; + """)) + ]); } private static string Exports(string members, string extraUsing = "") diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/Usage/HostWidthPointerSizeAnalyzerTests.cs b/tests/CheatEngine.SDK.Analyzers.Tests/Usage/HostWidthPointerSizeAnalyzerTests.cs index acbb02bc..7249ad66 100644 --- a/tests/CheatEngine.SDK.Analyzers.Tests/Usage/HostWidthPointerSizeAnalyzerTests.cs +++ b/tests/CheatEngine.SDK.Analyzers.Tests/Usage/HostWidthPointerSizeAnalyzerTests.cs @@ -105,7 +105,8 @@ internal static class Widths public static PointerSize Width() => {|#0:new PointerSize(IntPtr.Size)|}; } """, - new DiagnosticResult(DiagnosticDescriptors.HostWidthPointerSize).WithLocation(0).WithArguments("IntPtr.Size")); + new DiagnosticResult(DiagnosticDescriptors.HostWidthPointerSize).WithLocation(0) + .WithArguments("IntPtr.Size")); } [Fact] diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/packages.lock.json b/tests/CheatEngine.SDK.Analyzers.Tests/packages.lock.json index b3a73fef..45f87ce0 100644 --- a/tests/CheatEngine.SDK.Analyzers.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Analyzers.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.AotProbe/packages.lock.json b/tests/CheatEngine.SDK.AotProbe/packages.lock.json index 3b2f244a..43937847 100644 --- a/tests/CheatEngine.SDK.AotProbe/packages.lock.json +++ b/tests/CheatEngine.SDK.AotProbe/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.DotNet.ILCompiler": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Benchmarks/packages.lock.json b/tests/CheatEngine.SDK.Benchmarks/packages.lock.json index 9614c691..91743cf6 100644 --- a/tests/CheatEngine.SDK.Benchmarks/packages.lock.json +++ b/tests/CheatEngine.SDK.Benchmarks/packages.lock.json @@ -22,9 +22,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListExitTests.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListExitTests.cs index af98277a..3fba769c 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListExitTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListExitTests.cs @@ -68,9 +68,9 @@ public void Symbolic_address_expression_stays_distinct_from_the_resolved_current { using AddressListTestHost host = new(); CEObject handle = host.AddRecord(1, extra: """ - o.props.Address = "game.exe+10" - o.getters.getCurrentAddress = function() return function() return 0x140000010 end end - """); + o.props.Address = "game.exe+10" + o.getters.getCurrentAddress = function() return function() return 0x140000010 end end + """); MemoryRecord record = new(handle); Assert.True(record.TryGetAddressExpression(out string? expression)); diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListTestHost.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListTestHost.cs index cd64d577..4ac2e406 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListTestHost.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListTestHost.cs @@ -18,14 +18,13 @@ namespace CheatEngine.SDK.Engine.Tests.AddressList; internal sealed class AddressListTestHost : IDisposable { private readonly NativeLuaState _nativeState; - private readonly HostScope _scope; public AddressListTestHost() { EngineTest.RequireNativeLua(); _nativeState = new NativeLuaState(); - _scope = new HostScope(_nativeState); - State = _scope.State; + Scope = new HostScope(_nativeState); + State = Scope.State; CEObject list = FakeHost.CreateObject(State, "Probe"); FakeHost.SetGlobalObject(State, "list", list); EngineTest.Run(State, """ @@ -43,36 +42,45 @@ public LuaState State get; } - public HostScope Scope => _scope; + public HostScope Scope + { + get; + } + + public void Dispose() + { + Scope.Dispose(); + _nativeState.Dispose(); + } /// Creates a record with , adds it to records and returns its handle. public CEObject AddRecord(int id, string mode = "apply", bool active = false, string extra = "") { string initializer = string.Create(CultureInfo.InvariantCulture, $$""" - o.props.ID = {{id}} - o.props.Active = {{(active ? "true" : "false")}} - o.props.AsyncProcessing = false - o.props.Async = false - o.mode = "{{mode}}" - o.getters.destroy = function(o) - return function() - delete_calls = delete_calls + 1 - if o.gone then error("object already destroyed") end - o.gone = true - records[o.props.ID] = nil - end - end - o.setters.Active = function(o, value) - active_set_calls = active_set_calls + 1 - if o.mode == "refuse" then return end - if o.mode == "retry-request" then retry_requested = retry_requested + 1 return end - o.props.Active = value - if o.mode == "raise" then error("activation raised after it started") end - if o.mode == "async" then o.props.AsyncProcessing = true end - if o.mode == "post-read-fails" then o.getters.Active = function() error("record vanished") end end - end - {{extra}} - """); + o.props.ID = {{id}} + o.props.Active = {{(active ? "true" : "false")}} + o.props.AsyncProcessing = false + o.props.Async = false + o.mode = "{{mode}}" + o.getters.destroy = function(o) + return function() + delete_calls = delete_calls + 1 + if o.gone then error("object already destroyed") end + o.gone = true + records[o.props.ID] = nil + end + end + o.setters.Active = function(o, value) + active_set_calls = active_set_calls + 1 + if o.mode == "refuse" then return end + if o.mode == "retry-request" then retry_requested = retry_requested + 1 return end + o.props.Active = value + if o.mode == "raise" then error("activation raised after it started") end + if o.mode == "async" then o.props.AsyncProcessing = true end + if o.mode == "post-read-fails" then o.getters.Active = function() error("record vanished") end end + end + {{extra}} + """); CEObject record = FakeHost.CreateObject(State, "Probe", initializer); FakeHost.SetGlobalObject(State, "new_record", record); EngineTest.Run(State, Encoding.UTF8.GetBytes( @@ -93,10 +101,4 @@ public long ReadInteger(string name) Assert.True(State.TryGetGlobal(Encoding.UTF8.GetBytes(name)).IsOk); return EngineTest.ReadInteger(State, -1); } - - public void Dispose() - { - _scope.Dispose(); - _nativeState.Dispose(); - } } diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/MemoryRecordActivationTests.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/MemoryRecordActivationTests.cs index 7f2fa535..ec0c2fad 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/MemoryRecordActivationTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/MemoryRecordActivationTests.cs @@ -183,10 +183,10 @@ public void Memory_record_active_script_and_offset_count_reads_restore_the_stack { using AddressListTestHost host = new(); CEObject handle = host.AddRecord(1, active: true, extra: """ - o.props.Script = "[ENABLE]\n[DISABLE]" - o.props.OffsetCount = 2 - o.props.Async = true - """); + o.props.Script = "[ENABLE]\n[DISABLE]" + o.props.OffsetCount = 2 + o.props.Async = true + """); CEObject plain = host.AddRecord(2, extra: "o.props.OffsetCount = 'many'"); MemoryRecord record = new(handle); MemoryRecord other = new(plain); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Allocation/TargetMemoryAllocatorTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Allocation/TargetMemoryAllocatorTests.cs index bd647e2a..dc5002fc 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Allocation/TargetMemoryAllocatorTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Allocation/TargetMemoryAllocatorTests.cs @@ -378,7 +378,8 @@ public void TryAllocate_expected_failure_reports_not_applied_without_an_owner() TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.False(outcome.HasOwner); @@ -400,7 +401,8 @@ public void TryAllocate_unqualified_target_reports_not_started_before_the_effect TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.False(outcome.HasOwner); @@ -466,7 +468,8 @@ public void TryAllocate_with_only_the_compatibility_seam_refuses_without_an_effe TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.False(outcome.HasOwner); @@ -488,7 +491,8 @@ public void TryAllocate_success_without_an_address_is_a_marshalling_failure_with TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.Equal(EngineEffectState.Unknown, outcome.Effect); @@ -508,7 +512,8 @@ public void TryAllocate_failure_that_still_carries_an_address_keeps_it_and_repor TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.Equal(EngineEffectState.Unknown, outcome.Effect); @@ -528,7 +533,8 @@ public void TryAllocate_on_an_unqualified_target_after_the_effect_refuses_compen TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.False(outcome.HasOwner); @@ -554,7 +560,8 @@ public void TryAllocate_turns_a_binding_exception_into_an_outcome_without_parsin EngineFailureKind.GlobalUnavailable => new EngineGlobalUnavailableException("TargetMemoryAllocate"), EngineFailureKind.TargetIdentityUnavailable => new EngineTargetIdentityException("TargetMemoryAllocate", TargetSelection.CreateUnavailableCheck(TargetSelectionObservation.NoTarget())), - EngineFailureKind.ProtectedLuaFailure => new EngineLuaException("TargetMemoryAllocate", LuaStatus.SyntaxError, + EngineFailureKind.ProtectedLuaFailure => new EngineLuaException("TargetMemoryAllocate", + LuaStatus.SyntaxError, "A deliberately irrelevant localized message."), EngineFailureKind.BindingFailure => new EngineBindingException("TargetMemoryAllocate", "incompatible"), _ => new EngineMarshallingException("TargetMemoryAllocate", EngineMarshallingDirection.Result, "a", "b") @@ -566,7 +573,8 @@ public void TryAllocate_turns_a_binding_exception_into_an_outcome_without_parsin TargetMemoryAllocator allocator = new(operations); TargetAllocationAcquireOutcome outcome = - allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), out AllocatedRegion? region); + allocator.TryAllocate(new TargetAllocationRequest(new TargetAllocationSize(4096)), + out AllocatedRegion? region); Assert.Null(region); Assert.Equal(expectedEffect, outcome.Effect); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerDisableInfoSnapshotTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerDisableInfoSnapshotTests.cs index 13ea8eec..e1a2bb3c 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerDisableInfoSnapshotTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerDisableInfoSnapshotTests.cs @@ -36,9 +36,11 @@ public void Snapshot_projects_allocations_symbols_exception_ranges_and_registere } """u8); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); - AutoAssemblerDisableInfoSnapshot snapshot = Assert.IsType(outcome.DisableInfo); + AutoAssemblerDisableInfoSnapshot + snapshot = Assert.IsType(outcome.DisableInfo); Assert.Equal(AutoAssemblerDisableInfoSnapshotStatus.Complete, snapshot.Status); Assert.Collection(snapshot.Allocations, static code => @@ -58,7 +60,10 @@ public void Snapshot_projects_allocations_symbols_exception_ranges_and_registere Assert.Equal(["player_base", "ammo"], snapshot.RegisteredSymbols); Assert.Equal([new Address(0x140000010), new Address(0x140000020)], snapshot.ExceptionRanges); Assert.Equal( - [new AutoAssemblerSymbolInfo("newmem", new Address(0x140000000)), new AutoAssemblerSymbolInfo("return_here", new Address(0x140001005))], + [ + new AutoAssemblerSymbolInfo("newmem", new Address(0x140000000)), + new AutoAssemblerSymbolInfo("return_here", new Address(0x140001005)) + ], snapshot.Symbols); Assert.False(snapshot.HasCCodeSymbolList); Assert.IsNotType(snapshot.Allocations); @@ -84,12 +89,14 @@ public void Snapshot_sorts_entries_by_ordinal_name() } """u8); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); AutoAssemblerDisableInfoSnapshot snapshot = outcome.DisableInfo!; Assert.Equal(["B", "_z", "a", "b"], snapshot.Allocations.Select(static allocation => allocation.Name), StringComparer.Ordinal); - Assert.Equal(["B", "_z", "a", "b"], snapshot.Symbols.Select(static symbol => symbol.Name), StringComparer.Ordinal); + Assert.Equal(["B", "_z", "a", "b"], snapshot.Symbols.Select(static symbol => symbol.Name), + StringComparer.Ordinal); patch!.Dispose(); Assert.Equal(0, L.Top); } @@ -110,7 +117,8 @@ public void Snapshot_keeps_addresses_above_2_pow_63_exactly() } """u8); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); AutoAssemblerDisableInfoSnapshot snapshot = outcome.DisableInfo!; Assert.Equal(AutoAssemblerDisableInfoSnapshotStatus.Complete, snapshot.Status); @@ -181,7 +189,8 @@ public void Malformed_disable_info_publishes_the_patch_with_a_malformed_snapshot } """u8); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); Assert.Equal(AutoAssemblerApplyOutcomeKind.Applied, outcome.Kind); Assert.Equal(EngineEffectState.Applied, outcome.Effect); @@ -215,7 +224,8 @@ public void Ccode_symbol_list_is_reported_but_never_owned_or_destroyed() EngineTest.Run(L, "aa_disable_info = { ccodesymbols = aa_ccode_symbols }"u8); long destroyed = FakeHost.DestroyedCount(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); patch!.Dispose(); Assert.True(outcome.DisableInfo!.HasCCodeSymbolList); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerOutcomeTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerOutcomeTests.cs index 11aa453d..d4f86a44 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerOutcomeTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerOutcomeTests.cs @@ -1,3 +1,5 @@ +using System.Text; + using CheatEngine.SDK.Engine.Assembly; using CheatEngine.SDK.Engine.Objects; using CheatEngine.SDK.Engine.Targets; @@ -30,7 +32,8 @@ public void TryApplyWithOutcome_applied_publishes_the_patch_with_its_origin_and_ LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); Assert.Equal(AutoAssemblerApplyOutcomeKind.Applied, outcome.Kind); Assert.Equal(EngineEffectState.Applied, outcome.Effect); @@ -38,7 +41,8 @@ public void TryApplyWithOutcome_applied_publishes_the_patch_with_its_origin_and_ Assert.Null(outcome.Compensation); Assert.True(outcome.TargetObservation.IsQualified); Assert.True(outcome.PostEffectTargetCheck.GetValueOrDefault().IsCurrent); - AutoAssemblerDisableInfoSnapshot snapshot = Assert.IsType(outcome.DisableInfo); + AutoAssemblerDisableInfoSnapshot + snapshot = Assert.IsType(outcome.DisableInfo); Assert.Equal(AutoAssemblerDisableInfoSnapshotStatus.Complete, snapshot.Status); AutoAssemblerPatch owner = Assert.IsType(patch); Assert.Same(snapshot, owner.DisableInfo); @@ -64,7 +68,8 @@ public void TryApplyWithOutcome_rejected_reports_unknown_effect_and_no_owner() LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("apply-false", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("apply-false", out AutoAssemblerPatch? patch); Assert.Equal(AutoAssemblerApplyOutcomeKind.Rejected, outcome.Kind); Assert.Equal(EngineEffectState.Unknown, outcome.Effect); @@ -112,7 +117,7 @@ public void TryApplyWithOutcome_diagnostic_is_truncated_at_a_scalar_boundary(str using HostScope scope = new(state); LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - EngineTest.Run(L, System.Text.Encoding.UTF8.GetBytes("aa_long_detail = \"" + luaEscapedDetail + "\"")); + EngineTest.Run(L, Encoding.UTF8.GetBytes("aa_long_detail = \"" + luaEscapedDetail + "\"")); AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("apply-reject-long", new AutoAssemblerOptions { CaptureHostText = true, MaxHostTextBytes = maxBytes }, out _); @@ -210,7 +215,8 @@ public void TryApplyWithOutcome_success_without_warnings_reports_none() LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", SCapture, out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", SCapture, out AutoAssemblerPatch? patch); Assert.False(outcome.HasHostWarnings); Assert.Null(outcome.HostWarnings); @@ -230,7 +236,8 @@ public void TryApplyWithOutcome_success_without_a_table_is_invalid_result_with_u LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome(script, out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome(script, out AutoAssemblerPatch? patch); Assert.Equal(AutoAssemblerApplyOutcomeKind.InvalidResult, outcome.Kind); Assert.Equal(EngineEffectState.Unknown, outcome.Effect); @@ -292,7 +299,8 @@ public void TryApplyWithOutcome_publication_failure_reports_one_compensation_and Assert.False(outcome.HasPatch); Assert.IsType(cause); Assert.Equal(TargetReleaseStatus.Released, outcome.Compensation.GetValueOrDefault().Status); - AutoAssemblerDisableInfoSnapshot snapshot = Assert.IsType(outcome.DisableInfo); + AutoAssemblerDisableInfoSnapshot + snapshot = Assert.IsType(outcome.DisableInfo); Assert.Equal("newmem", Assert.Single(snapshot.Symbols).Name); Assert.Equal(1, AutoAssemblerTestHost.ReadCounter(L, "auto_assembler_disable_count")); Assert.True(AutoAssemblerTestHost.ReadBoolean(L, "auto_assembler_disable_received_info")); @@ -308,7 +316,8 @@ public void TryApplyWithOutcome_missing_global_reports_not_started_and_restores_ LuaState L = scope.State; FakeHost.InstallQualifiedLocalTarget(L); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); AutoAssemblerCheckOutcome check = AutoAssemblerPatcher.TryCheck("success", true); Assert.Equal(AutoAssemblerApplyOutcomeKind.GlobalUnavailable, outcome.Kind); @@ -355,7 +364,8 @@ public void TryApplyWithOutcome_unqualified_target_reports_not_started_before_an AutoAssemblerTestHost.Install(L); AutoAssemblerTestHost.SelectTarget(L, 0); - AutoAssemblerApplyOutcome outcome = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); + AutoAssemblerApplyOutcome outcome = + AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); Assert.Equal(AutoAssemblerApplyOutcomeKind.TargetIdentityUnavailable, outcome.Kind); Assert.Equal(EngineEffectState.NotStarted, outcome.Effect); @@ -374,7 +384,8 @@ public void TryApplyWithOutcome_protected_failure_reports_unknown_effect_and_rec LuaState L = scope.State; AutoAssemblerTestHost.Install(L); - AutoAssemblerApplyOutcome failed = AutoAssemblerPatcher.TryApplyWithOutcome("apply-raise", out AutoAssemblerPatch? none); + AutoAssemblerApplyOutcome failed = + AutoAssemblerPatcher.TryApplyWithOutcome("apply-raise", out AutoAssemblerPatch? none); Assert.Equal(0, L.Top); AutoAssemblerApplyOutcome recovered = AutoAssemblerPatcher.TryApplyWithOutcome("success", out AutoAssemblerPatch? patch); @@ -398,7 +409,8 @@ public void TryApplyWithOutcome_localized_error_text_never_changes_the_category( AutoAssemblerTestHost.Install(L); AutoAssemblerApplyOutcome english = AutoAssemblerPatcher.TryApplyWithOutcome("apply-false", SCapture, out _); - AutoAssemblerApplyOutcome french = AutoAssemblerPatcher.TryApplyWithOutcome("apply-reject-french", SCapture, out _); + AutoAssemblerApplyOutcome french = + AutoAssemblerPatcher.TryApplyWithOutcome("apply-reject-french", SCapture, out _); Assert.Equal(english.Kind, french.Kind); Assert.Equal(english.Effect, french.Effect); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerPatcherTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerPatcherTests.cs index e68c3c3b..0dd1c134 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerPatcherTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Assembly/AutoAssemblerPatcherTests.cs @@ -189,7 +189,8 @@ public void ReleaseWithTargetOutcome_when_the_current_target_differs_refuses_wit LuaRef disableInfo = scope.State.CreateRef(); int originalTargetId = Environment.ProcessId == 1 ? 2 : 1; AutoAssemblerPatch patch = new("success", disableInfo, - new EngineResourceOrigin(LuaRuntime.CurrentStateIdentity, new TargetProcessIncarnation(originalTargetId, 1)), + new EngineResourceOrigin(LuaRuntime.CurrentStateIdentity, + new TargetProcessIncarnation(originalTargetId, 1)), snapshot, null); TargetReleaseOutcome outcome = patch.ReleaseWithTargetOutcome(); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs index 6500303b..07ca4452 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs @@ -103,7 +103,8 @@ public void observe_current_rejects_a_target_reported_in_neither_family(bool is6 [Fact] [Trait("Qualification", "Q32")] - public void observe_current_without_a_selected_target_reports_target_not_selected_although_the_probes_look_like_x64() + public void + observe_current_without_a_selected_target_reports_target_not_selected_although_the_probes_look_like_x64() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -204,7 +205,8 @@ function targetIsArm() InstructionOperationStatus failed = InstructionProfiles.TryObserveCurrent(out _); Assert.Equal(0, scope.State.Top); EngineTest.Run(scope.State, "raise_isa_probe = false"u8); - InstructionOperationStatus recovered = InstructionProfiles.TryObserveCurrent(out InstructionTargetProfile profile); + InstructionOperationStatus recovered = + InstructionProfiles.TryObserveCurrent(out InstructionTargetProfile profile); Assert.Equal(InstructionOperationStatus.LuaFailure, failed); Assert.Equal(InstructionOperationStatus.Success, recovered); @@ -418,7 +420,9 @@ public void assemble_reports_a_target_change_after_the_effect_without_copying_by InstallInstructionGlobals(scope.State); InstructionTargetProfile targetProfile = Observe(scope.State); EngineTest.Run(scope.State, Encoding.UTF8.GetBytes("instruction_assemble_behavior = \"target-change\"\n" + - "instruction_assemble_next_process_id = " + selectionAfterEffect.ToString(CultureInfo.InvariantCulture))); + "instruction_assemble_next_process_id = " + + selectionAfterEffect.ToString(CultureInfo + .InvariantCulture))); Span destination = stackalloc byte[1]; destination[0] = 0xA5; diff --git a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolLeaseReplacementTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolLeaseReplacementTests.cs index 0e4622b3..fe6326a1 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolLeaseReplacementTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolLeaseReplacementTests.cs @@ -1,3 +1,5 @@ +using System.Text; + using CheatEngine.SDK.Engine.Inspection; using CheatEngine.SDK.Engine.Tests.Support; using CheatEngine.SDK.Engine.Values; @@ -246,15 +248,15 @@ public LuaState State get; } - public void Execute(string source) - { - EngineTest.Run(State, System.Text.Encoding.UTF8.GetBytes(source)); - } - public void Dispose() { _scope.Dispose(); _nativeState.Dispose(); } + + public void Execute(string source) + { + EngineTest.Run(State, Encoding.UTF8.GetBytes(source)); + } } } diff --git a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolListTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolListTests.cs index a4258a39..3504b9e7 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolListTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolListTests.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Text; using CheatEngine.SDK.Annotations.Lifetime; using CheatEngine.SDK.Engine.Inspection; @@ -120,7 +121,7 @@ public void Symbol_lookup_distinguishes_not_found_from_a_malformed_symbol_table( LuaOperationStatus badProcessId = list.TryGetProcessId(out _); Assert.Equal(LuaOperationStatusKind.NilResult, notFound.Kind); - Assert.Equal(default(SymbolInfo), none); + Assert.Equal(default, none); Assert.Equal(LuaOperationStatusKind.NilResult, noName.Kind); Assert.Null(name); Assert.True(processId.IsSuccess); @@ -361,7 +362,7 @@ public void Main_symbol_list_is_borrowed_only() foreach (Type type in typeof(SymbolList).Assembly.GetExportedTypes()) { foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | - BindingFlags.Static | BindingFlags.DeclaredOnly)) + BindingFlags.Static | BindingFlags.DeclaredOnly)) { bool takesBorrowedList = (type == typeof(SymbolList) && !method.IsStatic) || Array.Exists(method.GetParameters(), @@ -376,7 +377,8 @@ public void Main_symbol_list_is_borrowed_only() } Assert.True(offenders.Count == 0, - "A public method registers, unregisters or destroys a borrowed SymbolList: " + string.Join(", ", offenders)); + "A public method registers, unregisters or destroys a borrowed SymbolList: " + + string.Join(", ", offenders)); } [Fact] @@ -422,6 +424,12 @@ public LuaState State public LuaHostBinding Binding => _scope.Binding; + public void Dispose() + { + _scope.Dispose(); + _nativeState.Dispose(); + } + /// Creates a list that the next createSymbolList() call returns. public CEObject StageCreatedList(string initializer = "") { @@ -433,13 +441,7 @@ public CEObject StageCreatedList(string initializer = "") public void Execute(string source) { - EngineTest.Run(State, System.Text.Encoding.UTF8.GetBytes(source)); - } - - public void Dispose() - { - _scope.Dispose(); - _nativeState.Dispose(); + EngineTest.Run(State, Encoding.UTF8.GetBytes(source)); } } } diff --git a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs index 02242162..3a879839 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs @@ -17,21 +17,21 @@ public sealed class SymbolRegistryTests { /// A symbol handler double: registration, removal and name lookup share one table. internal static ReadOnlySpan SymbolHandler => """ - registered_symbols = {} - registrations = 0 - removals = 0 - registerSymbol = function(name, address, doNotSave) - registrations = registrations + 1 - registered_symbols[name] = address - end - unregisterSymbol = function(name) - removals = removals + 1 - registered_symbols[name] = nil - end - getAddressSafe = function(name, isLocal, shallow) - return registered_symbols[name] - end - """u8; + registered_symbols = {} + registrations = 0 + removals = 0 + registerSymbol = function(name, address, doNotSave) + registrations = registrations + 1 + registered_symbols[name] = address + end + unregisterSymbol = function(name) + removals = removals + 1 + registered_symbols[name] = nil + end + getAddressSafe = function(name, isLocal, shallow) + return registered_symbols[name] + end + """u8; [Fact] public void TryGetName_forwards_only_the_source_mapped_address() @@ -224,7 +224,8 @@ public void Owned_registration_publish_failure_compensates_once_and_preserves_th SymbolRegistrationHandoffException exception = Assert.Throws(() => SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default, - static (name, address, options, identity) => new SymbolRegistrationLease(name, address, options, identity), + static (name, address, options, identity) => + new SymbolRegistrationLease(name, address, options, identity), (_, _) => throw cause)); Assert.Same(cause, exception.InnerException); @@ -283,7 +284,8 @@ public void Owned_registration_publication_failure_with_unavailable_unregister_p SymbolRegistrationHandoffException exception = Assert.Throws(() => SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default, - static (name, address, options, identity) => new SymbolRegistrationLease(name, address, options, identity), + static (name, address, options, identity) => + new SymbolRegistrationLease(name, address, options, identity), (_, _) => throw cause)); Assert.Same(cause, exception.InnerException); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Memory/AddressTypeSeparationTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Memory/AddressTypeSeparationTests.cs index 18ae454d..357fa4db 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Memory/AddressTypeSeparationTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Memory/AddressTypeSeparationTests.cs @@ -29,7 +29,7 @@ public void Address_types_expose_no_conversion_to_each_other_or_to_double() string[] conversions = [ .. types.SelectMany(static type => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.DeclaredOnly)) + BindingFlags.Static | BindingFlags.DeclaredOnly)) .Where(static method => method.Name is "op_Implicit" or "op_Explicit") .Select(Describe) .Order(StringComparer.Ordinal) diff --git a/tests/CheatEngine.SDK.Engine.Tests/Processes/ProcessOperationStatusTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Processes/ProcessOperationStatusTests.cs index a56eba41..07ae2e10 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Processes/ProcessOperationStatusTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Processes/ProcessOperationStatusTests.cs @@ -39,7 +39,8 @@ public void named_statuses_carry_their_kind_and_only_success_reports_success() Assert.Equal(kind == ProcessOperationStatusKind.Success, status.IsSuccess); } - Assert.Equal(LuaStatus.RuntimeError, ProcessOperationStatus.ProtectedLuaFailure(LuaStatus.RuntimeError).LuaStatus); + Assert.Equal(LuaStatus.RuntimeError, + ProcessOperationStatus.ProtectedLuaFailure(LuaStatus.RuntimeError).LuaStatus); Assert.Throws(() => ProcessOperationStatus.ProtectedLuaFailure(LuaStatus.Ok)); } } diff --git a/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs index 4c8ae58b..0233766b 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs @@ -574,7 +574,8 @@ public void observe_target_architecture_reports_the_file_as_process_sentinel_wit } [Theory] - [InlineData("isConnectedToCEServer", "error('fixture probe failure')", ProcessOperationStatusKind.ProtectedLuaFailure)] + [InlineData("isConnectedToCEServer", "error('fixture probe failure')", + ProcessOperationStatusKind.ProtectedLuaFailure)] [InlineData("isConnectedToCEServer", "return nil", ProcessOperationStatusKind.InvalidResult)] [InlineData("isConnectedToCEServer", "return 'false'", ProcessOperationStatusKind.InvalidResult)] [InlineData("targetIsX86", "error('fixture probe failure')", ProcessOperationStatusKind.ProtectedLuaFailure)] @@ -712,7 +713,8 @@ public void configured_pointer_size_is_not_read_without_a_selected_target() using HostScope scope = new(state); // With no target CE 7.7 still answers getPointerSize() == 8 (spike C3 D2); the SDK must not report it. FakeHost.InstallCe77X64TargetFacts(scope.State, 0); - EngineTest.Run(scope.State, "function getPointerSize() error('getPointerSize must not be read without a target') end"u8); + EngineTest.Run(scope.State, + "function getPointerSize() error('getPointerSize must not be read without a target') end"u8); ProcessOperationStatus status = RuntimeProcessOperations.TryGetConfiguredPointerSize(out int rawBytes, out PointerSize pointerSize); @@ -769,7 +771,8 @@ public void configured_pointer_size_reads_only_the_pid_and_get_pointer_size(stri [InlineData("getPointerSize = nil", ProcessOperationStatusKind.GlobalUnavailable)] [InlineData("rt_process_id = 4294967295", ProcessOperationStatusKind.FileAsProcessTarget)] [InlineData("function getPointerSize() rt_process_id = 77 return 8 end", ProcessOperationStatusKind.TargetChanged)] - [InlineData("function getPointerSize() error('fixture failure') end", ProcessOperationStatusKind.ProtectedLuaFailure)] + [InlineData("function getPointerSize() error('fixture failure') end", + ProcessOperationStatusKind.ProtectedLuaFailure)] public void configured_pointer_size_keeps_availability_backend_change_and_lua_failures_distinct(string fixture, ProcessOperationStatusKind expected) { diff --git a/tests/CheatEngine.SDK.Engine.Tests/README.md b/tests/CheatEngine.SDK.Engine.Tests/README.md index 545158ba..11a6b421 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/README.md +++ b/tests/CheatEngine.SDK.Engine.Tests/README.md @@ -46,8 +46,8 @@ behavior. - A typed operation acquires the state once and pushes the object once (`HostCallCountTests`). - `Owned` destroys the object exactly once. After the plugin is disabled, `Dispose` throws and leaves the object alive; after a re-enable or a controlled state replacement, every release path consumes the owner without a call and - reports `RefusedRuntimeChanged`. `ReleaseWithOutcome` never throws, and a transfer keeps the origin - (`OwnedTests`, `EngineResourceOriginTests`, `OwnershipSurfaceTests`). + reports `RefusedRuntimeChanged`. `ReleaseWithOutcome` never throws, and a transfer keeps the origin (`OwnedTests`, + `EngineResourceOriginTests`, `OwnershipSurfaceTests`). - Enum values and Cheat Engine names are pinned by literals. - `RuntimeInfo`/`RuntimeCapabilities` retain explicit unknown facts. `TargetMemory` and `HostMemory` keep their address types separate, preserve byte ordering through span calls, and distinguish expected read/write failures. diff --git a/tests/CheatEngine.SDK.Engine.Tests/Runtime/RuntimeObservationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Runtime/RuntimeObservationsTests.cs index 18bbc5ab..7b75e2c3 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Runtime/RuntimeObservationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Runtime/RuntimeObservationsTests.cs @@ -77,7 +77,8 @@ public void try_observe_runtime_info_with_the_spike_x64_facts_produces_a_complet [Fact] [Trait("Qualification", "Q31.a")] - public void try_observe_runtime_info_with_a_configured_pointer_size_of_four_on_x64_reports_it_as_the_pointer_size_and_keeps_bitness() + public void + try_observe_runtime_info_with_a_configured_pointer_size_of_four_on_x64_reports_it_as_the_pointer_size_and_keeps_bitness() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -296,7 +297,8 @@ public void try_observe_runtime_info_on_a_detached_runtime_retains_the_lifecycle Assert.Throws(() => RuntimeObservations.TryObserveRuntimeInfo(out _)); Assert.Throws(() => RuntimeHostOperations.ObserveHost(out _)); Assert.Throws(() => RuntimeProcessOperations.ObserveTargetArchitecture(out _)); - Assert.Throws(() => RuntimeProcessOperations.TryGetConfiguredPointerSize(out _, out _)); + Assert.Throws(() => + RuntimeProcessOperations.TryGetConfiguredPointerSize(out _, out _)); } private static RuntimeCapabilityContract Contract(RuntimeInfo info, RuntimeCapabilityId capability) diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobBoundedScanTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobBoundedScanTests.cs index ee432f99..161c3c47 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobBoundedScanTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobBoundedScanTests.cs @@ -75,7 +75,7 @@ public void TryScanWithinBounds_refuses_invalid_bounds_before_any_CE_call() LuaState L = scope.State; _ = MemScanTestHost.Install(L); MemScanTestHost.ClearTrace(L); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; AobBoundedScanResult result = Scan(default, destination); @@ -112,7 +112,8 @@ public void TryScanWithinBounds_rejects_a_null_pattern_and_an_empty_destination_ [Fact] [Trait("Qualification", "Q28")] - public void TryScanWithinBounds_passes_the_bounds_verbatim_as_integers_and_disables_only_one_result_before_the_first_scan() + public void + TryScanWithinBounds_passes_the_bounds_verbatim_as_integers_and_disables_only_one_result_before_the_first_scan() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -130,7 +131,8 @@ public void TryScanWithinBounds_passes_the_bounds_verbatim_as_integers_and_disab MemScanTestHost.AssertLua(L, "first_scan_args[4] == '55 48 89 E5' and first_scan_args[5] == ''"); MemScanTestHost.AssertLua(L, "first_scan_start_type == 'integer' and first_scan_args[6] == 0x100000000"); MemScanTestHost.AssertLua(L, "first_scan_stop_type == 'integer' and first_scan_args[7] == math.mininteger"); - MemScanTestHost.AssertLua(L, "first_scan_args[8] == '' and first_scan_args[9] == 0 and first_scan_args[10] == ''"); + MemScanTestHost.AssertLua(L, + "first_scan_args[8] == '' and first_scan_args[9] == 0 and first_scan_args[10] == ''"); MemScanTestHost.AssertLua(L, "first_scan_args[11] == true and first_scan_args[12] == false"); MemScanTestHost.AssertLua(L, "first_scan_args[13] == false and first_scan_args[14] == false"); Assert.Equal(0L, MemScanTestHost.ReadInteger(L, "create_mem_scan_argument_count")); @@ -147,7 +149,8 @@ public void TryScanWithinBounds_passes_explicit_protection_and_alignment_options LuaState L = scope.State; _ = MemScanTestHost.Install(L); - _ = AobScanner.TryScanWithinBounds(Pattern, ModuleBounds, new AobScanOptions("+X-C-W", FastScanMethod.Aligned, "4"), + _ = AobScanner.TryScanWithinBounds(Pattern, ModuleBounds, + new AobScanOptions("+X-C-W", FastScanMethod.Aligned, "4"), new Address[1], TestContext.Current.CancellationToken); MemScanTestHost.AssertLua(L, "first_scan_args[8] == '+X-C-W' and first_scan_args[9] == 1"); @@ -187,7 +190,7 @@ public void TryScanWithinBounds_zero_found_rows_with_an_empty_error_string_is_no using HostScope scope = new(state); LuaState L = scope.State; _ = MemScanTestHost.Install(L); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; AobBoundedScanResult result = Scan(ModuleBounds, destination); @@ -212,7 +215,7 @@ public void TryScanWithinBounds_zero_found_rows_with_a_host_error_string_is_host LuaState L = scope.State; _ = MemScanTestHost.Install(L); MemScanTestHost.Run(L, "scan_error_string = 'No readable memory found'"); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; AobBoundedScanResult result = Scan(ModuleBounds, destination); @@ -353,7 +356,7 @@ public void TryScanWithinBounds_publishes_nothing_when_cancelled_during_the_copy using CancellationTokenSource cancellation = new(); using FakeHost.PCallProbe probe = FakeHost.ReplaceFoundListGetAddressWithPCallProbe(L, objects.FoundList, cancellation.Cancel); - Address[] destination = [new Address(0xA11CE), new Address(0xA11CE), new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE), new(0xA11CE), new(0xA11CE)]; AobBoundedScanResult result = ScanWithToken(Bounds(0, 0x10000), destination, cancellation.Token); @@ -490,7 +493,7 @@ public void TryScanWithinBounds_staging_allocation_failure_releases_the_created_ SetAddresses(L, "100000000"); InsufficientMemoryException injected = new("injected staging allocation failure"); StagingPool pool = new(injected); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; InsufficientMemoryException thrown = Assert.Throws(() => AobBoundedScan.Run(Pattern, ModuleBounds, AobScanOptions.Default, null, destination, pool, @@ -546,7 +549,7 @@ public void TryScanWithinBounds_malformed_count_or_address_is_invalid_result_and LuaState L = scope.State; _ = MemScanTestHost.Install(L); MemScanTestHost.Run(L, mode); - Address[] destination = [new Address(0xA11CE), new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE), new(0xA11CE)]; AobBoundedScanResult result = Scan(ModuleBounds, destination); @@ -572,7 +575,7 @@ public void TryScanWithinBounds_target_change_during_the_scan_is_target_changed_ MemScanTestHost.Run(L, "scan_first_hook = function() opened_process_id = " + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture) + " end"); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; AobBoundedScanResult result = Scan(ModuleBounds, destination); @@ -707,7 +710,7 @@ public void TryScanWithinBounds_with_an_expired_deadline_terminates_cooperativel _ = MemScanTestHost.Install(L); SetAddresses(L, "100000000"); MemScanTestHost.Run(L, "scan_wait_modes = { 'false', 'true' }"); - Address[] destination = [new Address(0xA11CE)]; + Address[] destination = [new(0xA11CE)]; AobBoundedScanResult result = AobScanner.TryScanWithinBounds(Pattern, ModuleBounds, AobScanOptions.Default, TimeSpan.FromMilliseconds(100), destination, TestContext.Current.CancellationToken); @@ -869,22 +872,26 @@ public StagingPool(Exception? rentFailure) public int RentCount { - get; private set; + get; + private set; } public int ReturnCount { - get; private set; + get; + private set; } public Address[]? LastRented { - get; private set; + get; + private set; } public Address[]? LastReturned { - get; private set; + get; + private set; } public override Address[] Rent(int minimumLength) diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobFirstFoundScanTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobFirstFoundScanTests.cs index b6558f0b..96b4d002 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobFirstFoundScanTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobFirstFoundScanTests.cs @@ -106,7 +106,8 @@ public void TryFindFirstFoundWithinBounds_address_below_start_is_indeterminate_f using HostScope scope = new(state); LuaState L = scope.State; _ = MemScanTestHost.Install(L); - MemScanTestHost.Run(L, "only_result_mode = 'value'; only_result_value = " + reported.ToString(CultureInfo.InvariantCulture)); + MemScanTestHost.Run(L, + "only_result_mode = 'value'; only_result_value = " + reported.ToString(CultureInfo.InvariantCulture)); AobFirstFoundResult result = Find(Bounds(0x1_0000_1000, 0x1_0036_7000)); @@ -163,7 +164,8 @@ public void TryFindFirstFoundWithinBounds_keeps_the_64_bit_pattern_of_high_addre [InlineData("not-found", AobFirstFoundOutcomeKind.NotFound, "scan.getOnlyResult,list.destroy,scan.destroy")] [InlineData("invalid", AobFirstFoundOutcomeKind.InvalidResult, "scan.getOnlyResult,list.destroy,scan.destroy")] [InlineData("get-raises", AobFirstFoundOutcomeKind.ScanFailed, "scan.getOnlyResult,list.destroy,scan.destroy")] - [InlineData("set-raises", AobFirstFoundOutcomeKind.ScanFailed, "scan.setOnlyOneResult:true,list.destroy,scan.destroy")] + [InlineData("set-raises", AobFirstFoundOutcomeKind.ScanFailed, + "scan.setOnlyOneResult:true,list.destroy,scan.destroy")] [InlineData("first-raises", AobFirstFoundOutcomeKind.ScanFailed, "scan.first:14,scan.terminate:false,scan.wait:5000,list.destroy,scan.destroy")] [InlineData("wait-raises", AobFirstFoundOutcomeKind.ScanFailed, @@ -262,7 +264,8 @@ public void TryFindFirstFoundWithinBounds_target_change_during_the_scan_reports_ _ = MemScanTestHost.Install(L); MemScanTestHost.Run(L, "only_result_mode = 'value'; only_result_value = 0x100000000; " + "scan_first_hook = function() opened_process_id = " + - MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture) + " end"); + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture) + + " end"); AobFirstFoundResult result = Find(ModuleBounds); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobScannerPublicationTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobScannerPublicationTests.cs index 485d3127..8bb73cdb 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobScannerPublicationTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Aob/AobScannerPublicationTests.cs @@ -23,7 +23,7 @@ public void TryScanOutcome_publication_failure_destroys_the_raw_list_once_and_re using NativeLuaState state = new(); using HostScope scope = new(state); LuaState L = scope.State; - CEObject handle = CreateTracedList(L, "Probe", destroyRaises: false); + CEObject handle = CreateTracedList(L, "Probe", false); AobStringListTestHost.InstallAobScan(L, handle); InvalidOperationException injected = new("injected publication failure"); Owned? results = null; @@ -46,7 +46,7 @@ public void TryScanDetailed_publication_failure_destroys_the_raw_list_once_and_r using NativeLuaState state = new(); using HostScope scope = new(state); LuaState L = scope.State; - CEObject handle = CreateTracedList(L, "Probe", destroyRaises: false); + CEObject handle = CreateTracedList(L, "Probe", false); AobStringListTestHost.InstallAobScan(L, handle); InvalidOperationException injected = new("injected owner allocation failure"); Owned? results = null; @@ -63,13 +63,14 @@ public void TryScanDetailed_publication_failure_destroys_the_raw_list_once_and_r [Fact] [Trait("Qualification", "Q27")] - public void TryScanOutcome_publication_failure_with_a_refusing_destroy_rethrows_the_original_failure_without_retrying() + public void + TryScanOutcome_publication_failure_with_a_refusing_destroy_rethrows_the_original_failure_without_retrying() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); using HostScope scope = new(state); LuaState L = scope.State; - CEObject handle = CreateTracedList(L, "Stubborn", destroyRaises: true); + CEObject handle = CreateTracedList(L, "Stubborn", true); AobStringListTestHost.InstallAobScan(L, handle); InvalidOperationException injected = new("injected publication failure"); Owned? results = null; @@ -91,7 +92,7 @@ public void TryScanOutcome_publication_failure_with_a_lua_exception_is_rethrown_ using NativeLuaState state = new(); using HostScope scope = new(state); LuaState L = scope.State; - CEObject handle = CreateTracedList(L, "Probe", destroyRaises: false); + CEObject handle = CreateTracedList(L, "Probe", false); AobStringListTestHost.InstallAobScan(L, handle); LuaException injected = new("injected publication failure"); Owned? results = null; diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemScanTestHost.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemScanTestHost.cs index 8243d81c..1b13a45f 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemScanTestHost.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemScanTestHost.cs @@ -24,7 +24,8 @@ namespace CheatEngine.SDK.Engine.Tests.Scanning; /// factory.scan, factory.list: the two CE factories. /// /// scan.setOnlyOneResult:<v>, scan.first:<argument count>, -/// scan.next:<argument count>, scan.wait (no argument) or scan.wait:<timeout>, +/// scan.next:<argument count>, scan.wait (no argument) or scan.wait:<timeout> +/// , /// scan.terminate:<force>, scan.new, scan.getOnlyResult, scan.ErrorString (a /// property read), scan.set.<property>:<v> (a write to OnlyOneResult, /// IsUnique, OnGuiUpdate, OnScanDone or OnScanStart), scan.destroy. @@ -304,7 +305,8 @@ public static int FindOtherQualifiedProcessId() } } - Assert.True(selected != 0, "No other process with a readable start time exists; a target change cannot be modelled."); + Assert.True(selected != 0, + "No other process with a readable start time exists; a target change cannot be modelled."); return selected; } diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs index 1135b1ba..6cada640 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs @@ -330,7 +330,8 @@ public void TryCreateWithOutcome_refuses_an_unqualified_target_before_either_fac [Fact] [Trait("Qualification", "Q25")] - public void TryCreate_when_adoption_fails_and_the_child_rollback_raises_still_destroys_the_parent_once_and_rethrows_the_adoption_failure() + public void + TryCreate_when_adoption_fails_and_the_child_rollback_raises_still_destroys_the_parent_once_and_rethrows_the_adoption_failure() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -359,7 +360,8 @@ public void TryCreate_when_adoption_fails_and_the_child_rollback_raises_still_de [Fact] [Trait("Qualification", "Q25")] - public void TryCreate_when_the_parent_rollback_raises_after_a_child_failure_reports_rollback_unconfirmed_without_retrying() + public void + TryCreate_when_the_parent_rollback_raises_after_a_child_failure_reports_rollback_unconfirmed_without_retrying() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/ScanExperimentalApiTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/ScanExperimentalApiTests.cs index a4e8bb45..4d4d35e7 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/ScanExperimentalApiTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/ScanExperimentalApiTests.cs @@ -14,7 +14,8 @@ namespace CheatEngine.SDK.Engine.Tests.Scanning; /// public sealed class ScanExperimentalApiTests { - private const string UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; + private const string UrlFormat = + "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; [Fact] public void Scanning_experimental_members_are_exactly_the_documented_gates() diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionBatteryTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionBatteryTests.cs index 2a45b2a7..bbcec89b 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionBatteryTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionBatteryTests.cs @@ -32,7 +32,9 @@ public void Wait_after_the_target_was_replaced_invalidates_as_target_changed_wit using HostScope scope = new(state); LuaState L = scope.State; MemoryScanSession session = StartScanning(L); - MemScanTestHost.Run(L, "opened_process_id = " + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); + MemScanTestHost.Run(L, + "opened_process_id = " + + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); MemoryScanException failure = Assert.Throws(session.WaitForCompletion); @@ -188,7 +190,8 @@ public void Scan_sessions_never_install_progress_or_completion_callbacks() Assert.DoesNotContain("setOnlyOneResult", trace, StringComparison.Ordinal); Assert.Equal(0L, MemScanTestHost.ReadInteger(L, "create_mem_scan_argument_count")); Assert.Equal(1L, MemScanTestHost.ReadInteger(L, "create_found_list_argument_count")); - Assert.EndsWith("scan.terminate:false,scan.wait:5000,list.destroy,scan.destroy", trace, StringComparison.Ordinal); + Assert.EndsWith("scan.terminate:false,scan.wait:5000,list.destroy,scan.destroy", trace, + StringComparison.Ordinal); Assert.Equal(0, L.Top); } @@ -214,7 +217,8 @@ public void First_scan_failure_category_does_not_depend_on_the_lua_error_text(st Assert.Equal(MemoryScanFailureKind.LuaError, failure.FailureKind); Assert.Equal("MemoryScan.FirstScan", failure.Operation); - Assert.Equal("The protected Lua call for memory scan operation 'MemoryScan.FirstScan' failed.", failure.Message); + Assert.Equal("The protected Lua call for memory scan operation 'MemoryScan.FirstScan' failed.", + failure.Message); Assert.Equal(MemoryScanState.Invalidated, session.State); Assert.Equal(MemoryScanInvalidationReason.ProtectedLuaFailure, session.InvalidationReason); Assert.Equal(0, L.Top); diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionDeadlineTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionDeadlineTests.cs index d34add8b..d302ec5f 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionDeadlineTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionDeadlineTests.cs @@ -187,7 +187,8 @@ public void TryWaitForCompletion_lua_error_is_lua_failure_and_invalidates() [Fact] [Trait("Qualification", "Q26")] - public void TryWaitForCompletion_initialize_failure_after_completion_is_initialization_failed_and_never_exposes_results() + public void + TryWaitForCompletion_initialize_failure_after_completion_is_initialization_failed_and_never_exposes_results() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -215,7 +216,9 @@ public void TryWaitForCompletion_after_a_target_replacement_reports_the_mismatch using HostScope scope = new(state); LuaState L = scope.State; MemoryScanSession session = StartScanning(L); - MemScanTestHost.Run(L, "opened_process_id = " + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); + MemScanTestHost.Run(L, + "opened_process_id = " + + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); MemoryScanWaitStatus status = session.TryWaitForCompletion(Deadline); @@ -388,7 +391,9 @@ public void TryTerminateScan_with_a_replaced_target_is_not_invoked_and_makes_no_ using HostScope scope = new(state); LuaState L = scope.State; MemoryScanSession session = StartScanning(L); - MemScanTestHost.Run(L, "opened_process_id = " + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); + MemScanTestHost.Run(L, + "opened_process_id = " + + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); MemoryScanTerminationStatus status = session.TryTerminateScan(Deadline); @@ -545,7 +550,8 @@ public void TryWaitForCompletion_disposed_from_inside_the_wait_releases_once_aft LuaState L = scope.State; MemoryScanSession session = StartScanning(L); MemoryScanReleaseOutcome? inner = null; - using FakeHost.ManagedHookScope hook = FakeHost.InstallManagedHook(L, () => inner = session.ReleaseWithOutcome()); + using FakeHost.ManagedHookScope hook = + FakeHost.InstallManagedHook(L, () => inner = session.ReleaseWithOutcome()); InstallWaitHook(L); Assert.Throws(() => session.TryWaitForCompletion(Deadline)); @@ -571,7 +577,8 @@ public void TryWaitForCompletion_disposed_from_inside_a_timed_out_wait_stops_the LuaState L = scope.State; MemoryScanSession session = StartScanning(L); List inner = []; - using FakeHost.ManagedHookScope hook = FakeHost.InstallManagedHook(L, () => inner.Add(session.ReleaseWithOutcome())); + using FakeHost.ManagedHookScope hook = + FakeHost.InstallManagedHook(L, () => inner.Add(session.ReleaseWithOutcome())); InstallWaitHook(L); MemScanTestHost.Run(L, "scan_wait_modes = { 'false', 'true' }"); @@ -626,7 +633,8 @@ public void TryTerminateScan_disposed_from_inside_the_settle_wait_releases_once_ // Every wait runs the managed hook (CE pumping queued main-thread work), then records that the hook returned. private static void InstallWaitHook(LuaState state) { - MemScanTestHost.Run(state, "scan_wait_hook = function() managed_hook(); table.insert(trace, 'hook.returned') end"); + MemScanTestHost.Run(state, + "scan_wait_hook = function() managed_hook(); table.insert(trace, 'hook.returned') end"); } private static MemoryScanSession StartScanning(LuaState state) diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionReleaseTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionReleaseTests.cs index d474aafb..3df906a2 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionReleaseTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/Values/MemoryScanSessionReleaseTests.cs @@ -264,7 +264,9 @@ public void Release_on_a_replaced_target_while_scanning_refuses_every_CE_call_an using HostScope scope = new(state); LuaState L = scope.State; MemoryScanSession session = StartScanning(L); - MemScanTestHost.Run(L, "opened_process_id = " + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); + MemScanTestHost.Run(L, + "opened_process_id = " + + MemScanTestHost.FindOtherQualifiedProcessId().ToString(CultureInfo.InvariantCulture)); MemoryScanReleaseOutcome outcome = session.ReleaseWithOutcome(); @@ -312,7 +314,8 @@ public void Dispose_from_inside_the_release_settle_wait_makes_no_CE_call_and_the [Fact] [Trait("Qualification", "Q26")] - public void Dispose_from_inside_WaitForCompletion_releases_once_after_the_wait_returned_without_initializing_results() + public void + Dispose_from_inside_WaitForCompletion_releases_once_after_the_wait_returned_without_initializing_results() { EngineTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -425,7 +428,8 @@ private static void AssertReentrantRefusal(Exception? refusal, string operation) // Every wait runs the managed hook (CE pumping queued main-thread work), then records that the hook returned. private static void InstallWaitHook(LuaState state) { - MemScanTestHost.Run(state, "scan_wait_hook = function() managed_hook(); table.insert(trace, 'hook.returned') end"); + MemScanTestHost.Run(state, + "scan_wait_hook = function() managed_hook(); table.insert(trace, 'hook.returned') end"); } private static MemoryScanSession StartScanning(LuaState state) diff --git a/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.Scanning.cs b/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.Scanning.cs index 598b1742..04cd3151 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.Scanning.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.Scanning.cs @@ -29,7 +29,8 @@ public static ManagedHookScope InstallManagedHook(LuaState L, Action hook) } using LuaFrame frame = new(L); - L.PushUncheckedFunction(new LuaNativeFunction((nint) (delegate* unmanaged[Cdecl]) &RunManagedHook)); + L.PushUncheckedFunction( + new LuaNativeFunction((nint) (delegate* unmanaged[Cdecl]) &RunManagedHook)); Assert.True(L.TrySetGlobal("managed_hook"u8).IsOk); ManagedHookScope scope = new(hook); s_activeManagedHook = scope; diff --git a/tests/CheatEngine.SDK.Engine.Tests/Targets/TargetSelectionTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Targets/TargetSelectionTests.cs index 8deffef0..04279482 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Targets/TargetSelectionTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Targets/TargetSelectionTests.cs @@ -27,8 +27,9 @@ public void ObserveCurrent_with_the_current_process_id_returns_a_qualified_incar Assert.True(observation.IsQualified); Assert.True(observation.Incarnation.HasValue); Assert.Equal(Environment.ProcessId, observation.Incarnation.Value.ProcessId); - Assert.Equal(TargetIdentityEvidence.CheatEngineSelectedProcessId | TargetIdentityEvidence.LocalBackendConfirmed | - TargetIdentityEvidence.LocalProcessStartTime, observation.Evidence); + Assert.Equal(TargetIdentityEvidence.CheatEngineSelectedProcessId | + TargetIdentityEvidence.LocalBackendConfirmed | + TargetIdentityEvidence.LocalProcessStartTime, observation.Evidence); Assert.Equal(TargetBackend.LocalProcess, observation.Backend); Assert.True(TargetSelection.ValidateCurrent(observation.Incarnation.Value).IsCurrent); Assert.Equal(0, scope.State.Top); diff --git a/tests/CheatEngine.SDK.Engine.Tests/packages.lock.json b/tests/CheatEngine.SDK.Engine.Tests/packages.lock.json index ee88a87c..35041e9f 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Engine.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/CoexistencePluginLayout.cs b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/CoexistencePluginLayout.cs index d08813fe..bea15933 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/CoexistencePluginLayout.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/CoexistencePluginLayout.cs @@ -29,7 +29,8 @@ internal static class CoexistencePluginLayout private static string BinRoot => RequireMetadata("CoexistencePluginBinRoot"); - private static string LowercaseConfiguration => RequireMetadata("CoexistencePluginConfiguration").ToLowerInvariant(); + private static string LowercaseConfiguration => + RequireMetadata("CoexistencePluginConfiguration").ToLowerInvariant(); /// /// Builds the shared-layout scenario: copies both plugin output directories into one folder. Fails loudly, per @@ -41,8 +42,8 @@ internal static class CoexistencePluginLayout internal static string CreateSharedLayout(string destinationDirectory) { Directory.CreateDirectory(destinationDirectory); - CopyDirectoryInto(PluginADirectory, destinationDirectory, compareExisting: false); - CopyDirectoryInto(PluginBDirectory, destinationDirectory, compareExisting: true); + CopyDirectoryInto(PluginADirectory, destinationDirectory, false); + CopyDirectoryInto(PluginBDirectory, destinationDirectory, true); return destinationDirectory; } @@ -74,7 +75,8 @@ private static void CopyDirectoryInto(string sourceDirectory, string destination private static string RequireMetadata(string key) { - foreach (AssemblyMetadataAttribute attribute in typeof(CoexistencePluginLayout).Assembly.GetCustomAttributes()) + foreach (AssemblyMetadataAttribute attribute in typeof(CoexistencePluginLayout).Assembly + .GetCustomAttributes()) { if (string.Equals(attribute.Key, key, StringComparison.Ordinal) && !string.IsNullOrEmpty(attribute.Value)) { diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorAlcRoute.cs b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorAlcRoute.cs index 2647c895..779f48f4 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorAlcRoute.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorAlcRoute.cs @@ -7,5 +7,5 @@ internal enum NativeHostEmulatorAlcRoute Component, /// The undocumented hdt_load_assembly + hdt_get_function_pointer route into the default ALC. - Default, + Default } diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorRunner.cs b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorRunner.cs index d875062d..80000dcf 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorRunner.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorRunner.cs @@ -23,7 +23,8 @@ internal static NativeHostEmulatorResult Run( string exePath = Path.Combine(emulatorDirectory, "ce-host-emulator.exe"); if (!File.Exists(exePath)) { - throw new FileNotFoundException($"'{exePath}' does not exist: run tests/native-host-emulator/build.ps1 first.", exePath); + throw new FileNotFoundException( + $"'{exePath}' does not exist: run tests/native-host-emulator/build.ps1 first.", exePath); } string runtimeConfigPath = Path.Combine(emulatorDirectory, "ce-like.runtimeconfig.json"); @@ -34,7 +35,7 @@ internal static NativeHostEmulatorResult Run( UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, - CreateNoWindow = true, + CreateNoWindow = true }; AddArgument(startInfo, "--lua", luaPath); @@ -49,7 +50,7 @@ internal static NativeHostEmulatorResult Run( using Process process = new() { - StartInfo = startInfo, + StartInfo = startInfo }; process.Start(); string standardOutput = process.StandardOutput.ReadToEnd(); @@ -57,7 +58,7 @@ internal static NativeHostEmulatorResult Run( bool exited = process.WaitForExit((int) ProcessTimeout.TotalMilliseconds); if (!exited) { - process.Kill(entireProcessTree: true); + process.Kill(true); throw new TimeoutException($"'{exePath}' did not exit within {ProcessTimeout}."); } @@ -89,7 +90,8 @@ private static string ResolveDotNetRoot() DirectoryInfo? root = new DirectoryInfo(runtimeDirectory).Parent?.Parent?.Parent; if (root is null) { - throw new InvalidOperationException($"Could not derive a .NET root three levels above '{runtimeDirectory}'."); + throw new InvalidOperationException( + $"Could not derive a .NET root three levels above '{runtimeDirectory}'."); } return root.FullName; diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorTests.cs b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorTests.cs index 7900b3b1..bcce66e7 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorTests.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Coexistence/NativeHostEmulatorTests.cs @@ -19,8 +19,9 @@ public sealed unsafe partial class NativeHostEmulatorTests [Fact] public void Required_mode_rejects_an_absent_emulator_directory() { - InvalidOperationException exception = Assert.Throws( - () => NativeHostEmulatorEnvironment.ResolveDirectory(null, "true")); + InvalidOperationException exception = + Assert.Throws(() => + NativeHostEmulatorEnvironment.ResolveDirectory(null, "true")); Assert.Equal( $"'{NativeHostEmulatorEnvironment.RequiredEnvironmentVariable}=true' requires '{NativeHostEmulatorEnvironment.DirectoryEnvironmentVariable}' to name a built native host emulator directory.", @@ -36,13 +37,15 @@ public void Emulator_is_optional_when_required_mode_is_off() [Fact] public void Emulator_abi_header_declares_the_managed_record_sizes() { - string headerPath = Path.Combine(CoexistencePluginLayout.RepoRoot, "tests", "native-host-emulator", "ce_host_emulator_abi.h"); + string headerPath = Path.Combine(CoexistencePluginLayout.RepoRoot, "tests", "native-host-emulator", + "ce_host_emulator_abi.h"); string headerText = File.ReadAllText(headerPath); Dictionary declaredSizes = new(StringComparer.Ordinal); foreach (Match match in StaticAssertSizeOf().Matches(headerText)) { - declaredSizes[match.Groups["type"].Value] = int.Parse(match.Groups["size"].Value, CultureInfo.InvariantCulture); + declaredSizes[match.Groups["type"].Value] = + int.Parse(match.Groups["size"].Value, CultureInfo.InvariantCulture); } Assert.Equal(36, declaredSizes["CePluginInitRecord"]); @@ -79,7 +82,8 @@ public void Separate_folders_measure_distinct_hosting_instances_and_disabling_A_ Assert.Equal("nil", result.Facts["after_disable_a.a_identity_type"]); Assert.Equal("nil", result.Facts["after_disable_a.a_ping_type"]); Assert.Equal("ok", result.Facts["after_disable_a.b_identity_call"]); - Assert.True(TryParseInt(result.Facts["after_disable_a.b_ping"], out int pingAfterDisableA) && pingAfterDisableA > 0, + Assert.True( + TryParseInt(result.Facts["after_disable_a.b_ping"], out int pingAfterDisableA) && pingAfterDisableA > 0, DescribeFailure("Q09.b", result, factsPath)); } @@ -94,7 +98,8 @@ public void Shared_folder_under_the_component_route_is_measured_and_disabling_A_ return; } - string sharedDirectory = Path.Combine(Path.GetTempPath(), "cesdk-native-host-emulator-" + Guid.NewGuid().ToString("N")); + string sharedDirectory = + Path.Combine(Path.GetTempPath(), "cesdk-native-host-emulator-" + Guid.NewGuid().ToString("N")); try { CoexistencePluginLayout.CreateSharedLayout(sharedDirectory); @@ -134,7 +139,8 @@ public void Default_context_loading_rejects_the_second_plugin_deterministically( return; } - string sharedDirectory = Path.Combine(Path.GetTempPath(), "cesdk-native-host-emulator-" + Guid.NewGuid().ToString("N")); + string sharedDirectory = + Path.Combine(Path.GetTempPath(), "cesdk-native-host-emulator-" + Guid.NewGuid().ToString("N")); try { CoexistencePluginLayout.CreateSharedLayout(sharedDirectory); @@ -199,7 +205,8 @@ public void Reenabling_A_answers_with_a_new_epoch() Assert.True(0 == result.ExitCode, DescribeFailure("Q05", result, factsPath)); Assert.Equal("ok", result.Facts["a.enable.2"]); - Assert.True(TryParseInt(result.Facts["a.identity.epoch"], out int firstEpoch), DescribeFailure("Q05", result, factsPath)); + Assert.True(TryParseInt(result.Facts["a.identity.epoch"], out int firstEpoch), + DescribeFailure("Q05", result, factsPath)); Assert.True(TryParseInt(result.Facts["a.identity.epoch_after_reenable"], out int reenabledEpoch), DescribeFailure("Q05", result, factsPath)); Assert.True(reenabledEpoch > firstEpoch, DescribeFailure("Q05", result, factsPath)); @@ -215,7 +222,7 @@ public void Emulator_facts_contain_no_absolute_path() return; } - NativeHostEmulatorResult result = RunSeparateComponentScenario(emulatorDirectory, out string factsPath); + NativeHostEmulatorResult result = RunSeparateComponentScenario(emulatorDirectory, out _); foreach (KeyValuePair fact in result.Facts) { @@ -235,12 +242,17 @@ private static NativeHostEmulatorResult RunSeparateComponentScenario(string emul factsPath); } - private static void AssertBootstrapAndEnableSucceeded(NativeHostEmulatorResult result, string label, string factsPath) + private static void AssertBootstrapAndEnableSucceeded(NativeHostEmulatorResult result, string label, + string factsPath) { - Assert.True(string.Equals("ok", result.Facts[$"{label}.bootstrap.first"], StringComparison.Ordinal), DescribeFailure(label, result, factsPath)); - Assert.True(string.Equals("ok", result.Facts[$"{label}.bootstrap.second"], StringComparison.Ordinal), DescribeFailure(label, result, factsPath)); - Assert.True(string.Equals("ok", result.Facts[$"{label}.getversion"], StringComparison.Ordinal), DescribeFailure(label, result, factsPath)); - Assert.True(string.Equals("ok", result.Facts[$"{label}.enable.1"], StringComparison.Ordinal), DescribeFailure(label, result, factsPath)); + Assert.True(string.Equals("ok", result.Facts[$"{label}.bootstrap.first"], StringComparison.Ordinal), + DescribeFailure(label, result, factsPath)); + Assert.True(string.Equals("ok", result.Facts[$"{label}.bootstrap.second"], StringComparison.Ordinal), + DescribeFailure(label, result, factsPath)); + Assert.True(string.Equals("ok", result.Facts[$"{label}.getversion"], StringComparison.Ordinal), + DescribeFailure(label, result, factsPath)); + Assert.True(string.Equals("ok", result.Facts[$"{label}.enable.1"], StringComparison.Ordinal), + DescribeFailure(label, result, factsPath)); } private static bool TryParseInt(string text, out int value) @@ -266,7 +278,7 @@ private static void TryDeleteDirectory(string path) { if (Directory.Exists(path)) { - Directory.Delete(path, recursive: true); + Directory.Delete(path, true); } } catch (IOException) @@ -279,9 +291,10 @@ private static void TryDeleteDirectory(string path) } } - [GeneratedRegex(@"static_assert\(sizeof\((?\w+)\)\s*==\s*(?\d+)", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"static_assert\(sizeof\((?\w+)\)\s*==\s*(?\d+)", RegexOptions.CultureInvariant, 1000)] private static partial Regex StaticAssertSizeOf(); - [GeneratedRegex(@"[A-Za-z]:[\\/]|\\\\[A-Za-z0-9._-]+\\", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"[A-Za-z]:[\\/]|\\\\[A-Za-z0-9._-]+\\", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)] private static partial Regex AbsolutePathPattern(); } diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/HostLogContainmentTests.cs b/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/HostLogContainmentTests.cs index 8405ae9a..8cba803f 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/HostLogContainmentTests.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/HostLogContainmentTests.cs @@ -11,7 +11,7 @@ namespace CheatEngine.SDK.Hosting.Tests.Diagnostics; /// ABI callback. It cannot recurse into , cannot re-enter a lifecycle transition, and /// cannot acquire Lua while a transition owns admission; none of that waits for the sink. /// -public sealed unsafe class HostLogContainmentTests : IDisposable +public sealed class HostLogContainmentTests : IDisposable { public HostLogContainmentTests() { diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/LoadIdentificationTests.cs b/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/LoadIdentificationTests.cs index 71e50026..0d8d9b53 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/LoadIdentificationTests.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Diagnostics/LoadIdentificationTests.cs @@ -19,13 +19,6 @@ public sealed unsafe partial class LoadIdentificationTests { private const string Prefix = "CheatEngineSdkIdentification: "; - [GeneratedRegex(@"lua\.module=(?[^;]*); lua\.sha256=(?[0-9a-f]{64}|unavailable)", - RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] - private static partial Regex LuaModuleFieldsPattern(); - - [GeneratedRegex("^[A-Za-z]:", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] - private static partial Regex DriveRootPattern(); - private static readonly string[] SExpectedKeys = [ "sdk.version", "sdk.commit", "sdk.consistent", "hosting.mvid", "hosting.alc", "plugin.id", "plugin.assembly", @@ -33,6 +26,13 @@ public sealed unsafe partial class LoadIdentificationTests "ce.fileVersion", "runtime", "arch" ]; + [GeneratedRegex(@"lua\.module=(?[^;]*); lua\.sha256=(?[0-9a-f]{64}|unavailable)", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)] + private static partial Regex LuaModuleFieldsPattern(); + + [GeneratedRegex("^[A-Za-z]:", RegexOptions.CultureInvariant, 1000)] + private static partial Regex DriveRootPattern(); + [Fact] public void Identification_is_not_emitted_unless_opted_in() { @@ -83,7 +83,8 @@ public void Identification_contains_the_sdk_version_and_a_40_hex_commit_or_unkno { string line = EnableWithIdentification(); - Assert.Matches(@"sdk\.version=[^;]*; sdk\.commit=(?:[0-9a-f]{40}|unknown); sdk\.consistent=(?:true|false)(?:; |$)", + Assert.Matches( + @"sdk\.version=[^;]*; sdk\.commit=(?:[0-9a-f]{40}|unknown); sdk\.consistent=(?:true|false)(?:; |$)", line); } @@ -102,7 +103,7 @@ public void Identification_contains_the_bridge_fingerprint_or_unavailable() public void Identification_contains_the_lua_module_file_name_and_sha256_without_a_directory() { HostingTest.RequireNativeLua(); - string line = EnableWithIdentification(useNativeLuaModule: true); + string line = EnableWithIdentification(true); Match match = LuaModuleFieldsPattern().Match(line); Assert.True(match.Success, "lua.module/lua.sha256 not found in: " + line); @@ -119,7 +120,7 @@ public void Identification_contains_the_lua_module_file_name_and_sha256_without_ public void Identification_never_contains_a_directory_separator_drive_root_or_user_name() { HostingTest.RequireNativeLua(); - string line = EnableWithIdentification(useNativeLuaModule: true); + string line = EnableWithIdentification(true); foreach ((string _, string value) in ParsePairs(line)) { diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Lifecycle/DisablePluginTests.cs b/tests/CheatEngine.SDK.Hosting.Tests/Lifecycle/DisablePluginTests.cs index 92b7bb2a..09303a73 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Lifecycle/DisablePluginTests.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Lifecycle/DisablePluginTests.cs @@ -261,106 +261,6 @@ public void Disable_while_a_worker_waits_in_a_Lua_synchronize_dispatch_pumps_it_ .GetResult(); } - // A native `synchronize` stand-in that genuinely hops to the real calling (main) thread, unlike - // MainThreadDispatcher.DispatchOverrideForTests: the worker blocks inside an actual Lua call while Disable's - // CheckSynchronize drain busy-polls TryPumpQueuedCall from the main thread until the queued call has been - // pushed, run exactly once, and released (A08-28). A call before anything is queued, or after the one queued - // call already ran, is a harmless no-op: the drain loop calls it repeatedly while it spins. - private sealed unsafe class RealSynchronizeStandIn : IDisposable - { - private static ManualResetEventSlim? s_queued; - private static ManualResetEventSlim? s_completed; - private static LuaRef? s_pendingRef; - private static LuaStatus s_pumpedCallStatus; - private static int s_pumped; - - private RealSynchronizeStandIn() - { - } - - public static bool Pumped => Volatile.Read(ref s_pumped) != 0; - - /// Waits until the worker's synchronize call has actually queued a pending call. - public static bool WaitUntilQueued(TimeSpan timeout) - { - return s_queued!.Wait(timeout); - } - - public static RealSynchronizeStandIn Install(LuaState main) - { - s_queued = new ManualResetEventSlim(false); - s_completed = new ManualResetEventSlim(false); - s_pendingRef = null; - s_pumped = 0; - Assert.True(main.TryPushFunction(new LuaNativeFunction(&SynchronizeThunk)).IsOk); - Assert.True(main.TrySetGlobal("synchronize"u8).IsOk); - return new RealSynchronizeStandIn(); - } - - public static void TryPumpQueuedCall() - { - if (!s_queued!.Wait(0)) - { - return; - } - - LuaRef? pendingRef = Interlocked.Exchange(ref s_pendingRef, null); - if (pendingRef is null) - { - return; - } - - using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation(); - LuaState state = operation.State; - Assert.True(state.TryPushRef(pendingRef)); - s_pumpedCallStatus = state.TryCall(0, 0); - pendingRef.Release(state); - Interlocked.Exchange(ref s_pumped, 1); - s_completed!.Set(); - } - - public void Dispose() - { - if (Pumped) - { - Assert.True(s_pumpedCallStatus.IsOk, "The pumped call failed."); - } - - s_queued?.Dispose(); - s_completed?.Dispose(); - s_queued = null; - s_completed = null; - s_pendingRef = null; - } - - [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] - private static int SynchronizeThunk(nint pointer) - { - LuaState state = new(pointer); - try - { - if (!state.IsFunction(1)) - { - return LuaThunk.Fail(state, "the real stand-in expects a function argument"u8); - } - - state.PushValue(1); - s_pendingRef = state.CreateRef(); - s_queued!.Set(); - if (!s_completed!.Wait(TimeSpan.FromSeconds(5))) - { - return LuaThunk.Fail(state, "the main-thread drain never ran the queued call"u8); - } - - return 0; - } - catch (Exception exception) - { - return LuaThunk.Fail(state, exception); - } - } - } - [Fact] public void Disabling_while_disabled_is_a_no_op_reported_as_TRUE_with_a_warning() { @@ -575,4 +475,104 @@ private static int NoOpThunk(nint handle) { return 0; } + + // A native `synchronize` stand-in that genuinely hops to the real calling (main) thread, unlike + // MainThreadDispatcher.DispatchOverrideForTests: the worker blocks inside an actual Lua call while Disable's + // CheckSynchronize drain busy-polls TryPumpQueuedCall from the main thread until the queued call has been + // pushed, run exactly once, and released (A08-28). A call before anything is queued, or after the one queued + // call already ran, is a harmless no-op: the drain loop calls it repeatedly while it spins. + private sealed class RealSynchronizeStandIn : IDisposable + { + private static ManualResetEventSlim? s_queued; + private static ManualResetEventSlim? s_completed; + private static LuaRef? s_pendingRef; + private static LuaStatus s_pumpedCallStatus; + private static int s_pumped; + + private RealSynchronizeStandIn() + { + } + + public static bool Pumped => Volatile.Read(ref s_pumped) != 0; + + public void Dispose() + { + if (Pumped) + { + Assert.True(s_pumpedCallStatus.IsOk, "The pumped call failed."); + } + + s_queued?.Dispose(); + s_completed?.Dispose(); + s_queued = null; + s_completed = null; + s_pendingRef = null; + } + + /// Waits until the worker's synchronize call has actually queued a pending call. + public static bool WaitUntilQueued(TimeSpan timeout) + { + return s_queued!.Wait(timeout); + } + + public static RealSynchronizeStandIn Install(LuaState main) + { + s_queued = new ManualResetEventSlim(false); + s_completed = new ManualResetEventSlim(false); + s_pendingRef = null; + s_pumped = 0; + Assert.True(main.TryPushFunction(new LuaNativeFunction(&SynchronizeThunk)).IsOk); + Assert.True(main.TrySetGlobal("synchronize"u8).IsOk); + return new RealSynchronizeStandIn(); + } + + public static void TryPumpQueuedCall() + { + if (!s_queued!.Wait(0)) + { + return; + } + + LuaRef? pendingRef = Interlocked.Exchange(ref s_pendingRef, null); + if (pendingRef is null) + { + return; + } + + using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation(); + LuaState state = operation.State; + Assert.True(state.TryPushRef(pendingRef)); + s_pumpedCallStatus = state.TryCall(0, 0); + pendingRef.Release(state); + Interlocked.Exchange(ref s_pumped, 1); + s_completed!.Set(); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static int SynchronizeThunk(nint pointer) + { + LuaState state = new(pointer); + try + { + if (!state.IsFunction(1)) + { + return LuaThunk.Fail(state, "the real stand-in expects a function argument"u8); + } + + state.PushValue(1); + s_pendingRef = state.CreateRef(); + s_queued!.Set(); + if (!s_completed!.Wait(TimeSpan.FromSeconds(5))) + { + return LuaThunk.Fail(state, "the main-thread drain never ran the queued call"u8); + } + + return 0; + } + catch (Exception exception) + { + return LuaThunk.Fail(state, exception); + } + } + } } diff --git a/tests/CheatEngine.SDK.Hosting.Tests/Threading/MainThreadTests.cs b/tests/CheatEngine.SDK.Hosting.Tests/Threading/MainThreadTests.cs index 3256d9c6..7027423e 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/Threading/MainThreadTests.cs +++ b/tests/CheatEngine.SDK.Hosting.Tests/Threading/MainThreadTests.cs @@ -278,11 +278,58 @@ public void Invoke_from_a_worker_works_under_the_default_admission_policy() .GetResult(); } + private static void InstallSynchronizeStandIn(NativeLuaState state) + { + LuaState L = LuaRuntime.AcquireState(); + using LuaFrame frame = new(L); + LuaStatus status = L.TryExecute( + "synchronize_calls = 0; function synchronize(f, ...) synchronize_calls = synchronize_calls + 1; return f(...) end"u8, + 0); + Assert.True(status.IsOk, "stand-in install failed: " + LuaError.FromStack(L, status).Message); + Assert.Equal(0, LuaApi.lua_gettop(state.L)); + } + + private static long ReadGlobalInteger(NativeLuaState state, ReadOnlySpan name) + { + LuaState L = new(state.Pointer); + using LuaFrame frame = new(L); + Assert.True(L.TryGetGlobal(name).IsOk); + Assert.True(L.TryReadInteger(-1, out long value)); + return value; + } + + private static T RunOnWorker(Func work) + { + // Boxes, not captured locals: static analysis (S2583) does not see a lambda's write to a captured local. + // Join publishes the worker's writes to this thread. + StrongBox result = new(); + StrongBox crash = new(); + Thread worker = new(() => + { + try + { + result.Value = work(); + } + catch (Exception exception) + { + crash.Value = exception; + } + }); + worker.Start(); + worker.Join(); + if (crash.Value is { } failure) + { + throw new InvalidOperationException("The worker crashed.", failure); + } + + return result.Value!; + } + // A native `synchronize` stand-in that genuinely hops to the real calling (main) thread instead of running the // pushed closure inline: it hands the closure to PumpQueuedCall (run on this test's true main thread) and blocks // the worker until that pump has actually invoked it, so the test observes MainThread.Invoke's full round trip // rather than only the rejection path the other stand-in tests prove. - private sealed unsafe class HoppingSynchronizeStandIn : IDisposable + private sealed class HoppingSynchronizeStandIn : IDisposable { private static ManualResetEventSlim? s_queued; private static ManualResetEventSlim? s_completed; @@ -293,6 +340,17 @@ private HoppingSynchronizeStandIn() { } + public void Dispose() + { + Assert.True(s_pumpedCallStatus.IsOk, + s_pumpedCallStatus.IsOk ? string.Empty : "The pumped call failed."); + s_queued?.Dispose(); + s_completed?.Dispose(); + s_queued = null; + s_completed = null; + s_pendingRef = null; + } + public static HoppingSynchronizeStandIn Install(LuaState main) { s_queued = new ManualResetEventSlim(false); @@ -321,17 +379,6 @@ public static bool PumpQueuedCall(TimeSpan timeout) return true; } - public void Dispose() - { - Assert.True(s_pumpedCallStatus.IsOk, - s_pumpedCallStatus.IsOk ? string.Empty : "The pumped call failed."); - s_queued?.Dispose(); - s_completed?.Dispose(); - s_queued = null; - s_completed = null; - s_pendingRef = null; - } - [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] private static int SynchronizeThunk(nint pointer) { @@ -359,51 +406,4 @@ private static int SynchronizeThunk(nint pointer) } } } - - private static void InstallSynchronizeStandIn(NativeLuaState state) - { - LuaState L = LuaRuntime.AcquireState(); - using LuaFrame frame = new(L); - LuaStatus status = L.TryExecute( - "synchronize_calls = 0; function synchronize(f, ...) synchronize_calls = synchronize_calls + 1; return f(...) end"u8, - 0); - Assert.True(status.IsOk, "stand-in install failed: " + LuaError.FromStack(L, status).Message); - Assert.Equal(0, LuaApi.lua_gettop(state.L)); - } - - private static long ReadGlobalInteger(NativeLuaState state, ReadOnlySpan name) - { - LuaState L = new(state.Pointer); - using LuaFrame frame = new(L); - Assert.True(L.TryGetGlobal(name).IsOk); - Assert.True(L.TryReadInteger(-1, out long value)); - return value; - } - - private static T RunOnWorker(Func work) - { - // Boxes, not captured locals: static analysis (S2583) does not see a lambda's write to a captured local. - // Join publishes the worker's writes to this thread. - StrongBox result = new(); - StrongBox crash = new(); - Thread worker = new(() => - { - try - { - result.Value = work(); - } - catch (Exception exception) - { - crash.Value = exception; - } - }); - worker.Start(); - worker.Join(); - if (crash.Value is { } failure) - { - throw new InvalidOperationException("The worker crashed.", failure); - } - - return result.Value!; - } } diff --git a/tests/CheatEngine.SDK.Hosting.Tests/packages.lock.json b/tests/CheatEngine.SDK.Hosting.Tests/packages.lock.json index a2e6b3bd..a73955e9 100644 --- a/tests/CheatEngine.SDK.Hosting.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Hosting.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/packages.lock.json b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/packages.lock.json index 67bcb512..bf6a3511 100644 --- a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/packages.lock.json +++ b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/packages.lock.json b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/packages.lock.json index 67bcb512..bf6a3511 100644 --- a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/packages.lock.json +++ b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.LivePlugin/MemoryBindings.cs b/tests/CheatEngine.SDK.LivePlugin/MemoryBindings.cs index f84128d0..64b1475b 100644 --- a/tests/CheatEngine.SDK.LivePlugin/MemoryBindings.cs +++ b/tests/CheatEngine.SDK.LivePlugin/MemoryBindings.cs @@ -23,6 +23,7 @@ internal static partial class MemoryBindings /// internal static bool TryReadInt32(nuint address, out int value) { + value = 0; return TryReadInt32Raw(address, true, out value); } diff --git a/tests/CheatEngine.SDK.LivePlugin/README.md b/tests/CheatEngine.SDK.LivePlugin/README.md index ae087a73..1d410a92 100644 --- a/tests/CheatEngine.SDK.LivePlugin/README.md +++ b/tests/CheatEngine.SDK.LivePlugin/README.md @@ -55,10 +55,10 @@ land in `artifacts/obj/CheatEngine.SDK.LivePlugin/generated//`. Prerequisites: -| Need | Detail | -|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Platform | Windows x64 with the pinned CE `7.7.0.10621` host (verify SHA-256 `9727076DA50924E4A097B49A02155E4B34759269C3017FF31375364B8826EB4D`) and Sysinternals [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) | -| SDK | .NET SDK 10.0.401 exactly, the version `global.json` pins with `rollForward: disable` | +| Need | Detail | +|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Platform | Windows x64 with the pinned CE `7.7.0.10621` host (verify SHA-256 `9727076DA50924E4A097B49A02155E4B34759269C3017FF31375364B8826EB4D`) and Sysinternals [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) | +| SDK | .NET SDK 10.0.401 exactly, the version `global.json` pins with `rollForward: disable` | | Runtime policy | The inspected CE binary uses the `nethost`/`hostfxr` route. The workspace's .NET 10 `ce.runtimeconfig.json` is a local modification, not an installer baseline. Preserve the controlled host's existing policy and record it with the transcript, including its observed hash; do not treat this guide as permission to edit an installed CE runtime configuration. Microsoft documents `nethost`/`hostfxr` for framework-dependent components. | 1. Build the plugin with `dotnet build tests/CheatEngine.SDK.LivePlugin/CheatEngine.SDK.LivePlugin.csproj -c Release`. diff --git a/tests/CheatEngine.SDK.LivePlugin/packages.lock.json b/tests/CheatEngine.SDK.LivePlugin/packages.lock.json index 67bcb512..bf6a3511 100644 --- a/tests/CheatEngine.SDK.LivePlugin/packages.lock.json +++ b/tests/CheatEngine.SDK.LivePlugin/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.LiveProbe.Tests/LiveProbeStateTests.cs b/tests/CheatEngine.SDK.LiveProbe.Tests/LiveProbeStateTests.cs index 7a132ecc..72aae196 100644 --- a/tests/CheatEngine.SDK.LiveProbe.Tests/LiveProbeStateTests.cs +++ b/tests/CheatEngine.SDK.LiveProbe.Tests/LiveProbeStateTests.cs @@ -8,13 +8,15 @@ public sealed class LiveProbeStateTests public void CaptureHostProfile_when_authorization_expires_after_enable_returns_fresh_denial() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return evaluationCount == 1 ? AllowedAuthorization() : AuthorizationDecision.Denied("The authorization manifest has expired."); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => 401); bool wasCaptured = false; @@ -36,13 +38,15 @@ public void CaptureHostProfile_when_authorization_expires_after_enable_returns_f public void CaptureHostProfile_when_target_image_changes_after_enable_returns_fresh_denial() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return evaluationCount == 1 ? AllowedAuthorization() : AuthorizationDecision.Denied("The declared target SHA-256 differs from its live process image."); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => 401); bool wasCaptured = false; @@ -64,11 +68,13 @@ public void CaptureHostProfile_when_target_image_changes_after_enable_returns_fr public void CaptureHostProfile_when_ce_target_pid_changes_after_enable_returns_denial() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return AllowedAuthorization(); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => 401); bool wasCaptured = false; @@ -90,13 +96,15 @@ public void CaptureHostProfile_when_ce_target_pid_changes_after_enable_returns_d public void CaptureHostProfile_when_fresh_authorization_and_pid_match_passes_fresh_decision_to_capture() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return evaluationCount == 1 ? AuthorizationDecision.Denied("The manifest was not present during enable.") : AllowedAuthorization(); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => throw new InvalidOperationException("The denied enable state must not read CE's PID.")); @@ -113,13 +121,15 @@ public void CaptureHostProfile_when_fresh_authorization_and_pid_match_passes_fre public void TryRequireRuntimeAuthorization_when_manifest_expires_after_enable_rechecks_and_denies() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return evaluationCount == 1 ? AllowedAuthorization() : AuthorizationDecision.Denied("The authorization manifest has expired."); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => 401); bool isAllowed = LiveProbeState.TryRequireRuntimeAuthorization(evaluateAuthorization, @@ -137,11 +147,13 @@ public void TryRequireRuntimeAuthorization_when_manifest_expires_after_enable_re public void TryRequireRuntimeAuthorization_when_ce_target_pid_changes_after_enable_rechecks_and_denies() { int evaluationCount = 0; - Func evaluateAuthorization = () => + + AuthorizationDecision evaluateAuthorization() { evaluationCount++; return AllowedAuthorization(); - }; + } + LiveProbeState.ValidateAfterEnable(evaluateAuthorization, static () => 401); bool isAllowed = LiveProbeState.TryRequireRuntimeAuthorization(evaluateAuthorization, static () => 402, diff --git a/tests/CheatEngine.SDK.LiveProbe.Tests/README.md b/tests/CheatEngine.SDK.LiveProbe.Tests/README.md index c8e03c22..999125ef 100644 --- a/tests/CheatEngine.SDK.LiveProbe.Tests/README.md +++ b/tests/CheatEngine.SDK.LiveProbe.Tests/README.md @@ -23,12 +23,12 @@ namespace, so it neither loads the plugin nor runs its Lua source generator. Tes CE's opened PID, the fault-switch file reader and the `PluginHost` facts. `AssemblyInfo.cs` disables parallelization because the harness keeps process-wide static state. -| Test class | Covers | -|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------| -| `LiveProbeStateTests` | Fresh authorization and target-PID checks before host-profile capture and every protected command. | -| `LiveProbeStatusTests` | Text and JSON status (plugin id, epoch, exports size, raw second bootstrap integer), the exception hook and the pump hook. | -| `LiveProbeFaultInjectionTests` | The `liveprobe.fault.json` switch: never read without authorization, exact stage selection, ignored and reported failures. | -| `HostProfileObservationTests` | Typed outcomes for missing, locked, vanishing or protected identity files; the bridge record names the loaded module only. | +| Test class | Covers | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `LiveProbeStateTests` | Fresh authorization and target-PID checks before host-profile capture and every protected command. | +| `LiveProbeStatusTests` | Text and JSON status (plugin id, epoch, exports size, raw second bootstrap integer), the exception hook and the pump hook. | +| `LiveProbeFaultInjectionTests` | The `liveprobe.fault.json` switch: never read without authorization, exact stage selection, ignored and reported failures. | +| `HostProfileObservationTests` | Typed outcomes for missing, locked, vanishing or protected identity files; the bridge record names the loaded module only. | ## Promise diff --git a/tests/CheatEngine.SDK.LiveProbe.Tests/packages.lock.json b/tests/CheatEngine.SDK.LiveProbe.Tests/packages.lock.json index 9a8731e7..ea6892d9 100644 --- a/tests/CheatEngine.SDK.LiveProbe.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.LiveProbe.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.LiveProbe/CE77LiveProbeBootstrap.cs b/tests/CheatEngine.SDK.LiveProbe/CE77LiveProbeBootstrap.cs index e3543f50..52b62975 100644 --- a/tests/CheatEngine.SDK.LiveProbe/CE77LiveProbeBootstrap.cs +++ b/tests/CheatEngine.SDK.LiveProbe/CE77LiveProbeBootstrap.cs @@ -16,7 +16,7 @@ namespace CESDK; /// #pragma warning disable MA0048 // The file identifies the CE 7.7 probe; the host fixes the public bootstrap type name. #pragma warning disable MA0049 // CE's host requires CESDK.CESDK exactly. -public static unsafe class CESDK +public static class CESDK { /// /// CE's fixed managed component entry point. The second argument is recorded raw and deliberately has no @@ -34,7 +34,7 @@ public static int CEPluginInitialize(nint initRecord, int opaqueHostArgument) int result = PluginHost.InitializeManaged(initRecord, opaqueHostArgument); #else - int result = PluginHost.InitializeManaged(initRecord, opaqueHostArgument); + int result = PluginHost.InitializeManaged(initRecord, opaqueHostArgument); #endif LiveProbeState.TryWriteTailCanaryAfterPackedRecord(initRecord, result); return result; diff --git a/tests/CheatEngine.SDK.LiveProbe/Ce77LiveProbePlugin.cs b/tests/CheatEngine.SDK.LiveProbe/Ce77LiveProbePlugin.cs index b3e0f750..1f622949 100644 --- a/tests/CheatEngine.SDK.LiveProbe/Ce77LiveProbePlugin.cs +++ b/tests/CheatEngine.SDK.LiveProbe/Ce77LiveProbePlugin.cs @@ -3,6 +3,7 @@ using CheatEngine.SDK.Hosting.Diagnostics; using CheatEngine.SDK.Hosting.Plugin; using CheatEngine.SDK.Lua.Calls; +using CheatEngine.SDK.Lua.Runtime; using CheatEngine.SDK.Lua.State; namespace LiveProbe; @@ -20,7 +21,7 @@ internal sealed class Ce77LiveProbePlugin : CheatEnginePlugin /// protected override void OnEnable() { - LuaState state = CheatEngine.SDK.Lua.Runtime.LuaRuntime.AcquireState(); + LuaState state = LuaRuntime.AcquireState(); LuaStatus registration = ProbeConsole.RegisterLuaFunctions(state); HostLog.Write(registration.IsOk ? HostLogLevel.Information : HostLogLevel.Error, string.Create(CultureInfo.InvariantCulture, @@ -37,7 +38,7 @@ protected override void OnEnable() /// protected override void OnDisable() { - LuaState state = CheatEngine.SDK.Lua.Runtime.LuaRuntime.AcquireState(); + LuaState state = LuaRuntime.AcquireState(); LuaStatus registration = ProbeConsole.UnregisterLuaFunctions(state); HostLog.Write(registration.IsOk ? HostLogLevel.Information : HostLogLevel.Error, string.Create(CultureInfo.InvariantCulture, diff --git a/tests/CheatEngine.SDK.LiveProbe/LiveProbeStatusReport.cs b/tests/CheatEngine.SDK.LiveProbe/LiveProbeStatusReport.cs index 96cfbfea..fe943173 100644 --- a/tests/CheatEngine.SDK.LiveProbe/LiveProbeStatusReport.cs +++ b/tests/CheatEngine.SDK.LiveProbe/LiveProbeStatusReport.cs @@ -58,7 +58,8 @@ private static void WriteBootstrap(Utf8JsonWriter writer, in LiveProbeStatusSnap writer.WriteNumber("tailWrites", snapshot.TailWrites); if (snapshot.TailCanaryWritten) { - writer.WriteString("tailBeforeHex", snapshot.TailReadBeforeWrite.ToString("X8", CultureInfo.InvariantCulture)); + writer.WriteString("tailBeforeHex", + snapshot.TailReadBeforeWrite.ToString("X8", CultureInfo.InvariantCulture)); } else { diff --git a/tests/CheatEngine.SDK.LiveProbe/README.md b/tests/CheatEngine.SDK.LiveProbe/README.md index b926acb5..1cde4a78 100644 --- a/tests/CheatEngine.SDK.LiveProbe/README.md +++ b/tests/CheatEngine.SDK.LiveProbe/README.md @@ -3,7 +3,8 @@ `CheatEngine.SDK.LiveProbe` is a manually loaded **evidence harness**, not a unit-test project, sample plugin, package asset, or normal CI input. It records CE 7.7 behaviours that fixture tests cannot establish: the raw managed bootstrap argument, the disputed packed-record tail, `synchronize`, per-thread Lua states and registry sharing, external -`resetLuaState`, CE userdata, callback cleanup on plugin disable, and the Checkpoint B hooks of the qualification runner. +`resetLuaState`, CE userdata, callback cleanup on plugin disable, and the Checkpoint B hooks of the qualification +runner. ## Objective @@ -17,7 +18,8 @@ non-ASCII plugin name (Q05.a) and where the SDK assemblies were loaded from (Q40 Only a real Cheat Engine host can show these behaviours, and C1/C2 success is never host evidence. The harness keeps every probe opt-in and fail-closed, so -loading it by mistake observes nothing and changes nothing. The solution compiles it, so a compile break is caught by CI; +loading it by mistake observes nothing and changes nothing. The solution compiles it, so a compile break is caught by +CI; CI never loads or runs it. ## Safety boundary @@ -46,17 +48,17 @@ containing sensitive paths, or captured process memory in source control. ## How it works -| File | Content | -|-----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `CE77LiveProbeBootstrap.cs` | The hand-written `CESDK.CESDK.CEPluginInitialize(nint, int)`. It records the second integer raw, then forwards to `PluginHost.InitializeManaged`. With `LIVEPROBE_NON_ASCII_NAME` it selects the non-ASCII factory. | -| `ProbePluginFactory.cs`, `ProbePluginFactoryNonAscii.cs` | The factories. `Create` first evaluates the fault switch. The non-ASCII factory exists only in the `-p:LiveProbeNonAsciiName=true` build. | -| `Ce77LiveProbePlugin.cs` | `OnEnable` registers the console commands, re-checks the gates and applies the `OnEnable` fault; `OnDisable` unregisters them and applies the `OnDisable` fault. | -| `ProbeConsole.cs`, `ProbeHostGlobals.cs` | The `[LuaFunction]` console commands below and the one `[LuaGlobal("getOpenedProcessID")]` binding. | -| `LiveProbeAuthorization.cs`, `AuthorizationDecision.cs` | The fail-closed gate: exact host hash and version, operator acknowledgement, unexpired manifest, live hash-verified disposable target. | -| `LiveProbeState.cs` | Process-local observations and the command implementations. Every command that acts re-evaluates the gate first. | -| `LiveProbeFaultInjection.cs`, `LiveProbeFaultStage.cs`, `LiveProbeFaultDecision.cs` | The fault switch (see below). Lua-free, compile-linked into the tests. | -| `LiveProbeHostFacts.cs`, `LiveProbeStatusSnapshot.cs`, `LiveProbeStatusReport.cs` | The status record: `PluginHost` facts, gates, fault decisions and observations serialized as `ce77-live-probe-status-v1` JSON. Lua-free, compile-linked into the tests. | -| `HostProfileObservation.cs` | The `ce77-live-host-profile-v1` identity record of host, Lua module, bridge, plugin and target files. | +| File | Content | +|-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `CE77LiveProbeBootstrap.cs` | The hand-written `CESDK.CESDK.CEPluginInitialize(nint, int)`. It records the second integer raw, then forwards to `PluginHost.InitializeManaged`. With `LIVEPROBE_NON_ASCII_NAME` it selects the non-ASCII factory. | +| `ProbePluginFactory.cs`, `ProbePluginFactoryNonAscii.cs` | The factories. `Create` first evaluates the fault switch. The non-ASCII factory exists only in the `-p:LiveProbeNonAsciiName=true` build. | +| `Ce77LiveProbePlugin.cs` | `OnEnable` registers the console commands, re-checks the gates and applies the `OnEnable` fault; `OnDisable` unregisters them and applies the `OnDisable` fault. | +| `ProbeConsole.cs`, `ProbeHostGlobals.cs` | The `[LuaFunction]` console commands below and the one `[LuaGlobal("getOpenedProcessID")]` binding. | +| `LiveProbeAuthorization.cs`, `AuthorizationDecision.cs` | The fail-closed gate: exact host hash and version, operator acknowledgement, unexpired manifest, live hash-verified disposable target. | +| `LiveProbeState.cs` | Process-local observations and the command implementations. Every command that acts re-evaluates the gate first. | +| `LiveProbeFaultInjection.cs`, `LiveProbeFaultStage.cs`, `LiveProbeFaultDecision.cs` | The fault switch (see below). Lua-free, compile-linked into the tests. | +| `LiveProbeHostFacts.cs`, `LiveProbeStatusSnapshot.cs`, `LiveProbeStatusReport.cs` | The status record: `PluginHost` facts, gates, fault decisions and observations serialized as `ce77-live-probe-status-v1` JSON. Lua-free, compile-linked into the tests. | +| `HostProfileObservation.cs` | The `ce77-live-host-profile-v1` identity record of host, Lua module, bridge, plugin and target files. | ### Console commands @@ -64,18 +66,18 @@ Every command returns a self-contained string. Commands that act (all except the authorization manifest, the target image and CE's opened PID immediately before acting, and return `Live probe denied: ` otherwise. -| Command | Observation | Used by | -|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------| -| `ce77_live_probe_status()` | Human-readable: bootstrap calls, the raw second integer (`opaqueSecondInt`, never labelled size or version), `PluginHost.LastInitRecordArgument`, phase, plugin id, epoch, reported exports size, gates, prior observations. | operator | -| `ce77_live_probe_status_json()` | The same facts as one `ce77-live-probe-status-v1` JSON object, plus assembly locations, MVIDs, the Hosting load context and every fault-switch decision. Not gated: it reads process-local facts only. | Q03, Q04, Q05, Q06, Q08, Q40 | -| `ce77_live_probe_host_profile()` | One `ce77-live-host-profile-v1` JSON identity record for the authorized CE host, loaded Lua module, loaded bridge, plugin and disposable target. | Q40 | -| `ce77_live_probe_throw_managed_exception()` | Throws `InvalidOperationException("CE 7.7 live probe deliberate managed exception (Q14).")` inside the generated thunk; call it under `pcall` and record the Lua error. | Q14 | -| `ce77_live_probe_pump_messages(seconds)` | Pumps CE's messages for 1–60 seconds from admitted main-thread work and returns a `ce77-live-probe-pump-v1` JSON record of the lifecycle phases seen. The operator unticks the plugin meanwhile. An observation, not a promise. | Q07 | -| `ce77_live_probe_begin_synchronize()` then `ce77_live_probe_synchronize_status()` | Worker, thunk and nested-invoke managed thread IDs; return round-trip and propagated exception. | operator | -| `ce77_live_probe_begin_lua_threads()` then `ce77_live_probe_lua_threads_status()` | GUI and worker `lua_State*` identities and a private raw-registry marker read by the worker. Do not execute other Lua for one second. | Q19 | -| `ce77_live_probe_snapshot_before_reset()` / `ce77_live_probe_snapshot_after_reset()` | State pointer, SDK epoch and reference slot before and after an operator-run `resetLuaState()`; whether the old SDK reference still pushed. The harness never calls the reset. | Q17, Q18 | -| `ce77_live_probe_userdata()` | `type(getMainForm())` and `tostring(getMainForm())`, without retaining the userdata or invoking the host-object pusher. | operator | -| `ce77_live_probe_prepare_callback_shutdown()` | Installs a counter callback and leaves it registered in `OnDisable`, so `LuaRuntime.Detach` must neutralize it. Call `pcall(ce77_live_probe_callback_shutdown)` before and after disabling. | Q15 | +| Command | Observation | Used by | +|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------| +| `ce77_live_probe_status()` | Human-readable: bootstrap calls, the raw second integer (`opaqueSecondInt`, never labelled size or version), `PluginHost.LastInitRecordArgument`, phase, plugin id, epoch, reported exports size, gates, prior observations. | operator | +| `ce77_live_probe_status_json()` | The same facts as one `ce77-live-probe-status-v1` JSON object, plus assembly locations, MVIDs, the Hosting load context and every fault-switch decision. Not gated: it reads process-local facts only. | Q03, Q04, Q05, Q06, Q08, Q40 | +| `ce77_live_probe_host_profile()` | One `ce77-live-host-profile-v1` JSON identity record for the authorized CE host, loaded Lua module, loaded bridge, plugin and disposable target. | Q40 | +| `ce77_live_probe_throw_managed_exception()` | Throws `InvalidOperationException("CE 7.7 live probe deliberate managed exception (Q14).")` inside the generated thunk; call it under `pcall` and record the Lua error. | Q14 | +| `ce77_live_probe_pump_messages(seconds)` | Pumps CE's messages for 1–60 seconds from admitted main-thread work and returns a `ce77-live-probe-pump-v1` JSON record of the lifecycle phases seen. The operator unticks the plugin meanwhile. An observation, not a promise. | Q07 | +| `ce77_live_probe_begin_synchronize()` then `ce77_live_probe_synchronize_status()` | Worker, thunk and nested-invoke managed thread IDs; return round-trip and propagated exception. | operator | +| `ce77_live_probe_begin_lua_threads()` then `ce77_live_probe_lua_threads_status()` | GUI and worker `lua_State*` identities and a private raw-registry marker read by the worker. Do not execute other Lua for one second. | Q19 | +| `ce77_live_probe_snapshot_before_reset()` / `ce77_live_probe_snapshot_after_reset()` | State pointer, SDK epoch and reference slot before and after an operator-run `resetLuaState()`; whether the old SDK reference still pushed. The harness never calls the reset. | Q17, Q18 | +| `ce77_live_probe_userdata()` | `type(getMainForm())` and `tostring(getMainForm())`, without retaining the userdata or invoking the host-object pusher. | operator | +| `ce77_live_probe_prepare_callback_shutdown()` | Installs a counter callback and leaves it registered in `OnDisable`, so `LuaRuntime.Detach` must neutralize it. Call `pcall(ce77_live_probe_callback_shutdown)` before and after disabling. | Q15 | ### Fault switch (Q06, Q08) @@ -88,7 +90,8 @@ A file named `liveprobe.fault.json` next to `CheatEngine.SDK.LiveProbe.dll` sele `throwIn` is `None`, `FactoryCreate` (the factory throws before constructing the plugin; the enable fails and the next enable tries again), `OnEnable` (after the console commands are registered) or `OnDisable` (after they are unregistered). The switch is read once per enable, without Lua, and only when the authorization gate allows: without -the manifest the file is never opened. An unknown schema, an unknown stage or malformed JSON is ignored and reported. Every +the manifest the file is never opened. An unknown schema, an unknown stage or malformed JSON is ignored and reported. +Every decision is logged and appears under `faultInjection` in `ce77_live_probe_status_json()`, with the list of stages that actually threw. The runner writes and deletes the file in the bundle folder; it never lives in the repository. @@ -97,8 +100,8 @@ actually threw. The runner writes and deletes the file in the bundle folder; it `dotnet build tests/CheatEngine.SDK.LiveProbe/CheatEngine.SDK.LiveProbe.csproj -c Release -p:LiveProbeNonAsciiName=true` defines `LIVEPROBE_NON_ASCII_NAME`, and the bootstrap then registers the name `CheatEngine.SDK Live Probe é 日本` (one Latin-1 character and two characters outside code page 1252). It settles, by observation, whether Cheat Engine 7.7 -decodes the name the SDK converts to the process ANSI code page -([`AnsiNameBuffer`](../../libs/CheatEngine.SDK.Hosting/Bootstrap/AnsiNameBuffer.cs)). The default build keeps the ASCII +decodes the name the SDK converts to the process ANSI code page ([ +`AnsiNameBuffer`](../../libs/CheatEngine.SDK.Hosting/Bootstrap/AnsiNameBuffer.cs)). The default build keeps the ASCII name `CheatEngine.SDK CE 7.7 Live Probe`. The switch writes to the same output folder, so rebuild without it afterwards. ## Build and load @@ -138,7 +141,8 @@ disposable target through its normal cleanup route. Do not force-unload assembli ## Promise - The harness is compiled by CI through `CheatEngine.SDK.slnx` as an x64 dynamic-loading plugin that is not a test - module and never packs (`QualificationHarnessShapeTests.LiveProbe_is_in_the_solution_as_an_x64_dynamic_loading_plugin_that_never_packs`). + module and never packs + (`QualificationHarnessShapeTests.LiveProbe_is_in_the_solution_as_an_x64_dynamic_loading_plugin_that_never_packs`). - A fresh authorization is required before every acting command, and a changed manifest, target image or CE target PID is refused (`LiveProbeStateTests`). - The raw second bootstrap integer and the reported exports size are reported without interpretation @@ -146,7 +150,8 @@ disposable target through its normal cleanup route. Do not force-unload assembli - Without authorization the exception and pump hooks are inert, and the pump refuses a duration outside 1–60 seconds before any host call (`LiveProbeStatusTests`). - The two status commands are deliberately not gated: they serialize process-local facts (bootstrap record, `PluginHost` - state, gate results, fault decisions, assembly identities), touch no target and make no Lua call of their own; an absent context is + state, gate results, fault decisions, assembly identities), touch no target and make no Lua call of their own; an + absent context is reported as absent, never as zero values (`LiveProbeStatusTests.Status_without_an_enabled_context_reports_none_instead_of_zero_values`, `LiveProbeStatusTests.Status_json_reports_the_fault_switch_decision_and_the_assembly_identities`). @@ -157,7 +162,8 @@ disposable target through its normal cleanup route. Do not force-unload assembli to the application (`HostProfileObservationTests.Host_profile_records_the_loaded_bridge_module_and_never_a_file_next_to_the_application`). - No live test is invoked by `dotnet test`, normal CI, Release validation or packaging: the solution only compiles it, - and no workflow references the runner (`LocalQualificationRunnerTests.No_workflow_references_the_local_qualification_runner`). + and no workflow references the runner + (`LocalQualificationRunnerTests.No_workflow_references_the_local_qualification_runner`). ## Run the tests diff --git a/tests/CheatEngine.SDK.LiveProbe/packages.lock.json b/tests/CheatEngine.SDK.LiveProbe/packages.lock.json index ca4a72fd..fa8a73e9 100644 --- a/tests/CheatEngine.SDK.LiveProbe/packages.lock.json +++ b/tests/CheatEngine.SDK.LiveProbe/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Lua.FailureProbe/packages.lock.json b/tests/CheatEngine.SDK.Lua.FailureProbe/packages.lock.json index 6dd54051..5356f467 100644 --- a/tests/CheatEngine.SDK.Lua.FailureProbe/packages.lock.json +++ b/tests/CheatEngine.SDK.Lua.FailureProbe/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaBridgeContractTests.cs b/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaBridgeContractTests.cs index 80f3f828..bca49f83 100644 --- a/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaBridgeContractTests.cs +++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaBridgeContractTests.cs @@ -94,7 +94,7 @@ private static bool HasDelayImports(string path) uint directories = ReadUInt32(image, optionalOffset + 108); Require(directories > 13, "The native bridge has no delay-import directory slot."); - int delayImportDirectoryOffset = optionalOffset + 112 + 13 * 8; + int delayImportDirectoryOffset = optionalOffset + 112 + (13 * 8); return ReadUInt32(image, delayImportDirectoryOffset) != 0 || ReadUInt32(image, delayImportDirectoryOffset + sizeof(uint)) != 0; } @@ -132,7 +132,7 @@ private static List ReadExportedNames(string path) for (uint i = 0u; i < namesCount; i++) { - uint nameRva = ReadUInt32(image, checked(namesOffset + (int) i * sizeof(uint))); + uint nameRva = ReadUInt32(image, checked(namesOffset + ((int) i * sizeof(uint)))); result.Add(ReadAsciiZ(image, RvaToFileOffset(image, nameRva, sizeOfHeaders, sectionOffset, sectionCount))); } @@ -193,7 +193,7 @@ private static int RvaToFileOffset(byte[] image, uint rva, uint sizeOfHeaders, i for (int i = 0; i < sectionCount; i++) { - int offset = checked(sectionOffset + i * 40); + int offset = checked(sectionOffset + (i * 40)); Require(offset <= image.Length - 40, "The native bridge has a truncated section header."); uint virtualSize = ReadUInt32(image, offset + 8); uint virtualAddress = ReadUInt32(image, offset + 12); diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaInteropPrimitiveMatrixTests.cs b/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaInteropPrimitiveMatrixTests.cs index 7d4818eb..041dc966 100644 --- a/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaInteropPrimitiveMatrixTests.cs +++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/Protected/LuaInteropPrimitiveMatrixTests.cs @@ -9,7 +9,8 @@ namespace CheatEngine.SDK.Lua.Interop.Tests.Protected; /// -/// The primitive matrix tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json (embedded) against the real +/// The primitive matrix tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json +/// (embedded) against the real /// : one row per public static member, the error class and stack effect its XML documentation /// states, the exports the function-pointer table binds, and the bridge catalogue's direct-call policy. DLL-free. /// @@ -43,8 +44,11 @@ public void Every_matrix_row_names_an_existing_LuaApi_member() { HashSet members = PublicMembers(); - string[] unknown = [.. Rows().Select(static row => row.GetProperty("member").GetString()!) - .Where(member => !members.Contains(member))]; + string[] unknown = + [ + .. Rows().Select(static row => row.GetProperty("member").GetString()!) + .Where(member => !members.Contains(member)) + ]; Assert.True(unknown.Length == 0, "Rows without a public static LuaApi member: " + string.Join(", ", unknown)); } @@ -53,7 +57,8 @@ public void Every_matrix_row_names_an_existing_LuaApi_member() public void Matrix_raises_class_equals_the_documented_raises_remark() { string path = Path.Combine(AppContext.BaseDirectory, DocumentationFile); - Assert.True(File.Exists(path), $"The XML documentation of CheatEngine.SDK.Lua.Interop was not copied to '{path}'."); + Assert.True(File.Exists(path), + $"The XML documentation of CheatEngine.SDK.Lua.Interop was not copied to '{path}'."); Dictionary remarks = ReadRemarks(XDocument.Load(path)); List problems = []; foreach (JsonElement row in Rows()) @@ -68,7 +73,8 @@ public void Matrix_raises_class_equals_the_documented_raises_remark() if (!string.Equals(expectedRaises, row.GetProperty("raises").GetString(), StringComparison.Ordinal) || !string.Equals(expectedStack, stack, StringComparison.Ordinal)) { - problems.Add($"{member}: documented {expectedStack ?? "no stack"} / {expectedRaises}, matrix {stack ?? "no stack"} / {row.GetProperty("raises").GetString()}."); + problems.Add( + $"{member}: documented {expectedStack ?? "no stack"} / {expectedRaises}, matrix {stack ?? "no stack"} / {row.GetProperty("raises").GetString()}."); } } @@ -122,12 +128,16 @@ private static void CheckPolicy(string member, JsonElement policy, JsonElement r string? bridge = Optional(row, "bridgeOperation"); string expectedDecision = policy.GetProperty("allowedDirectly").GetBoolean() ? "DirectAllowed" - : policy.TryGetProperty("conditionalDirectUse", out _) ? "ConditionallyDirect" : "BridgeRequired"; + : policy.TryGetProperty("conditionalDirectUse", out _) + ? "ConditionallyDirect" + : "BridgeRequired"; if (!string.Equals(expectedDecision, decision, StringComparison.Ordinal) || !string.Equals(Optional(policy, "bridgeOperation"), bridge, StringComparison.Ordinal) || - !string.Equals(policy.GetProperty("nativeSymbol").GetString(), Optional(row, "nativeSymbol"), StringComparison.Ordinal)) + !string.Equals(policy.GetProperty("nativeSymbol").GetString(), Optional(row, "nativeSymbol"), + StringComparison.Ordinal)) { - problems.Add($"{member}: the policy route ({expectedDecision}, {Optional(policy, "bridgeOperation")}) differs from the row ({decision}, {bridge})."); + problems.Add( + $"{member}: the policy route ({expectedDecision}, {Optional(policy, "bridgeOperation")}) differs from the row ({decision}, {bridge})."); } int policyRank = Rank(policy.GetProperty("raises").GetString()!); @@ -138,7 +148,8 @@ private static void CheckPolicy(string member, JsonElement policy, JsonElement r operation.TryGetProperty("provenanceConflict", out _); if (policyRank != rowRank && !(policyRank > rowRank && conflictRecorded)) { - problems.Add($"{member}: the policy classifies it {policy.GetProperty("raises").GetString()}, the matrix {row.GetProperty("raises").GetString()}."); + problems.Add( + $"{member}: the policy classifies it {policy.GetProperty("raises").GetString()}, the matrix {row.GetProperty("raises").GetString()}."); } } @@ -213,6 +224,6 @@ private static JsonDocument Load(string resource) } [GeneratedRegex(@"Stack: (?.*?)\. Raises: (?\w+)", RegexOptions.CultureInvariant, - matchTimeoutMilliseconds: 1000)] + 1000)] private static partial Regex StackAndRaises(); } diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/README.md b/tests/CheatEngine.SDK.Lua.Interop.Tests/README.md index 260e5dc1..3e84fd9b 100644 --- a/tests/CheatEngine.SDK.Lua.Interop.Tests/README.md +++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/README.md @@ -59,7 +59,8 @@ dotnet test --project tests/CheatEngine.SDK.Lua.Interop.Tests --filter-trait "Ca imports are explicitly cdecl and retain GC transitions (`LuaProtectedApiTests`). Native fixture checks additionally prove that an incomplete export table, negative input count, and input count above Lua's top return the bridge's no-error sentinel without changing the Lua stack (`LuaBridgeContractBoundaryTests`). -- The primitive matrix `tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json` has exactly one row per public static `LuaApi` +- The primitive matrix `tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json` has + exactly one row per public static `LuaApi` member, its error class and stack effect equal the member's `Raises:` and `Stack:` remarks, its native symbols are exactly the exports the table binds, and the catalogue's direct-call policy agrees with it (`LuaInteropPrimitiveMatrixTests`). diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/Support/IlReader.cs b/tests/CheatEngine.SDK.Lua.Interop.Tests/Support/IlReader.cs index d4aa61d4..3ec93411 100644 --- a/tests/CheatEngine.SDK.Lua.Interop.Tests/Support/IlReader.cs +++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/Support/IlReader.cs @@ -61,7 +61,7 @@ private static int OperandSize(OperandType type, byte[] il, int offset) OperandType.ShortInlineBrTarget or OperandType.ShortInlineI or OperandType.ShortInlineVar => 1, OperandType.InlineVar => 2, OperandType.InlineI8 or OperandType.InlineR => 8, - OperandType.InlineSwitch => 4 + 4 * BitConverter.ToInt32(il, offset), + OperandType.InlineSwitch => 4 + (4 * BitConverter.ToInt32(il, offset)), _ => 4 }; } diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/packages.lock.json b/tests/CheatEngine.SDK.Lua.Interop.Tests/packages.lock.json index c149a719..321c8dd6 100644 --- a/tests/CheatEngine.SDK.Lua.Interop.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Lua.Tests/CompilerServices/LuaCallSupportOptionalTests.cs b/tests/CheatEngine.SDK.Lua.Tests/CompilerServices/LuaCallSupportOptionalTests.cs index 9a0a7b4f..2aad6ad1 100644 --- a/tests/CheatEngine.SDK.Lua.Tests/CompilerServices/LuaCallSupportOptionalTests.cs +++ b/tests/CheatEngine.SDK.Lua.Tests/CompilerServices/LuaCallSupportOptionalTests.cs @@ -9,8 +9,10 @@ namespace CheatEngine.SDK.Lua.Tests.CompilerServices; /// /// The generator-facing helpers behind optional arguments, optional results and variadic results -/// (, , -/// ): omitted, nil and a value stay distinct, reads never +/// (, +/// , +/// ): omitted, nil and a value stay distinct, reads +/// never /// change the stack, and a failure is classified by type, never by text. /// [Trait("Category", "NativeLua")] @@ -73,14 +75,16 @@ public void ReadResults_copies_every_value_to_the_top_or_reports_the_needed_capa Span values = stackalloc long[4]; Span small = stackalloc long[2]; - Assert.Equal(LuaOperationStatus.Success, LuaCallSupport.ReadResults(L, 2, values, out int count)); + Assert.Equal(LuaOperationStatus.Success, + LuaCallSupport.ReadResults(L, 2, values, out int count)); Assert.Equal(3, count); Assert.Equal([10L, 20L, 30L], values[..3].ToArray()); Assert.Equal(LuaOperationStatus.ResultCapacityExceeded, LuaCallSupport.ReadResults(L, 2, small, out int needed)); Assert.Equal(3, needed); Assert.Equal([0L, 0L], small.ToArray()); - Assert.Equal(LuaOperationStatus.Success, LuaCallSupport.ReadResults(L, 5, small, out int none)); + Assert.Equal(LuaOperationStatus.Success, + LuaCallSupport.ReadResults(L, 5, small, out int none)); Assert.Equal(0, none); Assert.Throws(() => LuaCallSupport.ReadResults(L, 0, new long[1], out _)); @@ -98,7 +102,8 @@ public void ReadResults_classifies_a_nil_or_wrong_kind_value_and_clears_what_it_ L.PushNil(); Span values = stackalloc long[4]; - Assert.Equal(LuaOperationStatus.NilResult, LuaCallSupport.ReadResults(L, 1, values, out int nilCount)); + Assert.Equal(LuaOperationStatus.NilResult, + LuaCallSupport.ReadResults(L, 1, values, out int nilCount)); Assert.Equal(0, nilCount); Assert.Equal([0L, 0L, 0L, 0L], values.ToArray()); diff --git a/tests/CheatEngine.SDK.Lua.Tests/Marshalling/LuaOptionalTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Marshalling/LuaOptionalTests.cs index 327219de..266e23ec 100644 --- a/tests/CheatEngine.SDK.Lua.Tests/Marshalling/LuaOptionalTests.cs +++ b/tests/CheatEngine.SDK.Lua.Tests/Marshalling/LuaOptionalTests.cs @@ -78,7 +78,8 @@ public void Value_throws_unless_a_value_is_present() [Fact] public void Equality_distinguishes_the_three_states() { - LuaOptional[] states = [LuaOptional.Omitted(), LuaOptional.Nil(), LuaOptional.Of(0), LuaOptional.Of(1)]; + LuaOptional[] states = + [LuaOptional.Omitted(), LuaOptional.Nil(), LuaOptional.Of(0), LuaOptional.Of(1)]; for (int i = 0; i < states.Length; i++) { diff --git a/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFailureProcessTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFailureProcessTests.cs index 89ac3b29..28089e69 100644 --- a/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFailureProcessTests.cs +++ b/tests/CheatEngine.SDK.Lua.Tests/Protected/NativeFailureProcessTests.cs @@ -127,7 +127,8 @@ public async Task Allocation_failures_return_through_the_native_boundary() } /// - /// Data-driven from libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json: every operation that can raise names its + /// Data-driven from libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json: every operation that + /// can raise names its /// failure evidence, and every probe marker it names is printed by one run of the failure probe. The probe prints a /// marker only after the failure returned a status and the Lua stack was restored, so a missing marker is a /// failure path that no longer recovers. Adding a raising operation without evidence fails here. @@ -153,7 +154,8 @@ public async Task Every_catalogued_raising_operation_reports_its_failure_marker( $"Protected operation {id} can raise but names no failure evidence."); foreach (JsonElement item in evidence.EnumerateArray()) { - if (string.Equals(item.GetProperty("kind").GetString(), "FailureProbeMarker", StringComparison.Ordinal)) + if (string.Equals(item.GetProperty("kind").GetString(), "FailureProbeMarker", + StringComparison.Ordinal)) { markers.Add(item.GetProperty("marker").GetString()!); } @@ -173,7 +175,8 @@ private static JsonDocument LoadCatalogue() { const string ResourceName = "CheatEngine.SDK.Lua.Tests.ProtectedOperations.json"; using Stream stream = typeof(NativeFailureProcessTests).Assembly.GetManifestResourceStream(ResourceName) - ?? throw new InvalidOperationException($"The embedded resource {ResourceName} is missing."); + ?? throw new InvalidOperationException( + $"The embedded resource {ResourceName} is missing."); return JsonDocument.Parse(stream); } diff --git a/tests/CheatEngine.SDK.Lua.Tests/Runtime/LuaExternalResetDetectionTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Runtime/LuaExternalResetDetectionTests.cs index 656d93b1..0de8eb90 100644 --- a/tests/CheatEngine.SDK.Lua.Tests/Runtime/LuaExternalResetDetectionTests.cs +++ b/tests/CheatEngine.SDK.Lua.Tests/Runtime/LuaExternalResetDetectionTests.cs @@ -58,7 +58,8 @@ public void After_an_external_reset_every_admission_path_reports_ExternalStateRe Assert.False(LuaRuntime.TryAcquireOperation(out LuaRuntimeOperation viaTry)); viaTry.Dispose(); - InvalidOperationException thrown = Assert.Throws(() => LuaRuntime.AcquireOperation()); + InvalidOperationException + thrown = Assert.Throws(() => LuaRuntime.AcquireOperation()); Assert.Contains("replaced its Lua state", thrown.Message, StringComparison.Ordinal); Assert.Throws(() => LuaRuntime.AcquireOperation(main)); @@ -220,7 +221,8 @@ public void The_supported_reset_restamps_the_replacement_state_and_is_not_report [Fact] [Trait("Qualification", "Q17")] - public void A_state_replacement_during_a_registration_transaction_leaves_the_lease_stale_and_the_new_state_untouched() + public void + A_state_replacement_during_a_registration_transaction_leaves_the_lease_stale_and_the_new_state_untouched() { LuaTest.RequireNativeLua(); using NativeLuaState state = new(); @@ -228,7 +230,7 @@ public void A_state_replacement_during_a_registration_transaction_leaves_the_lea using RuntimeScope scope = new(state); using NativeLuaState replacement = new(); LuaState replacementView = new(replacement.L); - LuaRegistrationEntry[] entries = [new LuaRegistrationEntry("sdk013_reset_registration", Thunks.Add)]; + LuaRegistrationEntry[] entries = [new("sdk013_reset_registration", Thunks.Add)]; LuaRegistrationResult result = LuaRegistrationSet.Register(main, entries); Assert.True(result.IsSuccess); LuaRegistrationLease lease = result.Lease!; diff --git a/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json b/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json index be8d8b17..8db749f6 100644 --- a/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Lua.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.NativeAotLibraryProbe/packages.lock.json b/tests/CheatEngine.SDK.NativeAotLibraryProbe/packages.lock.json index d4ea7a8d..42b84e54 100644 --- a/tests/CheatEngine.SDK.NativeAotLibraryProbe/packages.lock.json +++ b/tests/CheatEngine.SDK.NativeAotLibraryProbe/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.DotNet.ILCompiler": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/LibraryProbeContractTests.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/LibraryProbeContractTests.cs index 28789c63..b3cca7ff 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/LibraryProbeContractTests.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/LibraryProbeContractTests.cs @@ -89,7 +89,10 @@ public void The_real_bridge_surface_is_refused_as_a_whole() LibraryProbeContract.FindViolations(PortableExecutableExportReader.ReadExportNames(BridgeImage.Load())); Assert.Equal(6, violations.Count); - Assert.Equal(2, violations.Count(static violation => violation.Contains("required fixture export", StringComparison.Ordinal))); - Assert.Equal(4, violations.Count(static violation => violation.Contains("unexpected export", StringComparison.Ordinal))); + Assert.Equal(2, + violations.Count(static violation => + violation.Contains("required fixture export", StringComparison.Ordinal))); + Assert.Equal(4, + violations.Count(static violation => violation.Contains("unexpected export", StringComparison.Ordinal))); } } diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/NativeAotLibraryProbeExportSurfaceTests.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/NativeAotLibraryProbeExportSurfaceTests.cs index b3a2f3b9..b9af912f 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/NativeAotLibraryProbeExportSurfaceTests.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/NativeAotLibraryProbeExportSurfaceTests.cs @@ -26,9 +26,11 @@ .. typeof(NativeAotLibraryProbeExports) .Order(StringComparer.Ordinal) ]; - Assert.Equal(NativeAotLibraryProbeExportNames.Required.Order(StringComparer.Ordinal), entryPoints, StringComparer.Ordinal); + Assert.Equal(NativeAotLibraryProbeExportNames.Required.Order(StringComparer.Ordinal), entryPoints, + StringComparer.Ordinal); Assert.DoesNotContain(entryPoints, static name => name.StartsWith(LibraryProbeContract.NativePluginPrefix, StringComparison.Ordinal)); - Assert.Empty(LibraryProbeContract.FindViolations([.. entryPoints, .. LibraryProbeContract.AllowedRuntimeExports])); + Assert.Empty( + LibraryProbeContract.FindViolations([.. entryPoints, .. LibraryProbeContract.AllowedRuntimeExports])); } } diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/PortableExecutableExportReaderTests.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/PortableExecutableExportReaderTests.cs index b08fb98d..f79516f3 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/PortableExecutableExportReaderTests.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/PortableExecutableExportReaderTests.cs @@ -1,3 +1,5 @@ +using System.Text; + using CheatEngine.SDK.NativeAotLoaderHarness.Tests.Support; namespace CheatEngine.SDK.NativeAotLoaderHarness.Tests; @@ -68,7 +70,7 @@ public void Refuses_truncated_bytes(int length) public void Refuses_an_image_truncated_inside_an_export_name() { byte[] full = BridgeImage.Load(); - int firstName = full.AsSpan().IndexOf(System.Text.Encoding.ASCII.GetBytes(BridgeImage.ExportNames[0] + "\0")); + int firstName = full.AsSpan().IndexOf(Encoding.ASCII.GetBytes(BridgeImage.ExportNames[0] + "\0")); Assert.True(firstName > 0); byte[] image = full[..(firstName + 3)]; diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md index e1e8bfe6..02b906db 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md @@ -34,8 +34,7 @@ win-x64, Release) exports exactly the two probe names and `DotNetRuntimeDebugHea - The reader returns the bridge's four export names and refuses a non-AMD64 image, an image without an export directory and truncated bytes (`PortableExecutableExportReaderTests`). - The contract refuses a `CEPlugin_*` export, including one read from patched bytes, a missing probe export and any - unexpected export, and accepts exactly the probe names plus the measured runtime export - (`LibraryProbeContractTests`). + unexpected export, and accepts exactly the probe names plus the measured runtime export (`LibraryProbeContractTests`). - The probe source declares exactly the two required entry points and no `CEPlugin_*` name (`NativeAotLibraryProbeExportSurfaceTests`). diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/Support/BridgeImage.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/Support/BridgeImage.cs index a32a384f..551b39aa 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/Support/BridgeImage.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/Support/BridgeImage.cs @@ -32,7 +32,8 @@ public static IReadOnlyList ExportNames public static byte[] Load() { using Stream stream = typeof(BridgeImage).Assembly.GetManifestResourceStream(ResourceName) - ?? throw new InvalidOperationException($"The embedded resource {ResourceName} is missing."); + ?? throw new InvalidOperationException( + $"The embedded resource {ResourceName} is missing."); using MemoryStream copy = new(); stream.CopyTo(copy); return copy.ToArray(); diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/packages.lock.json b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/packages.lock.json index fe279b25..b254c5b5 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness/PortableExecutableExportReader.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness/PortableExecutableExportReader.cs index c87820ac..3976737c 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness/PortableExecutableExportReader.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness/PortableExecutableExportReader.cs @@ -77,7 +77,7 @@ private static int ResolveFileOffset(PEHeaders headers, int relativeVirtualAddre } long fileOffset = section.PointerToRawData + (relativeVirtualAddress - sectionStart); - if (fileOffset < 0 || fileOffset > int.MaxValue) + if (fileOffset is < 0 or > int.MaxValue) { throw new InvalidOperationException("The export directory resolves outside the file."); } diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness/Program.cs b/tests/CheatEngine.SDK.NativeAotLoaderHarness/Program.cs index 8af8d9b7..a7711af1 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness/Program.cs +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness/Program.cs @@ -154,7 +154,10 @@ private static LibraryInspection ReadAndValidateProbe(string libraryPath) return new LibraryInspection(fullPath, sha256, LibraryProbeContract.RuntimeExportsIn(exportNames), fileLock); } - private sealed class LibraryInspection(string path, string sha256, IReadOnlyList runtimeExports, + private sealed class LibraryInspection( + string path, + string sha256, + IReadOnlyList runtimeExports, FileStream fileLock) : IDisposable { public string Path diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness/README.md b/tests/CheatEngine.SDK.NativeAotLoaderHarness/README.md index 61930236..214da015 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness/README.md +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness/README.md @@ -29,7 +29,8 @@ The harness has exactly two modes: Both modes first require the exact export surface of `LibraryProbeContract`: no `CEPlugin_` export (a classic Cheat Engine plugin entry point), both probe exports, and nothing else except the NativeAOT runtime's own -`DotNetRuntimeDebugHeader`, the one extra export a local `dotnet publish` of the probe produced. Any drift exits non-zero +`DotNetRuntimeDebugHeader`, the one extra export a local `dotnet publish` of the probe produced. Any drift exits +non-zero with `harness.error=` naming every violation; a conforming file prints `export.runtime.*`, `export.unexpected=none` and `contract=passed`. The load mode therefore refuses any file that has a `CEPlugin_` export, and it cannot be pointed at an arbitrary path or a production plugin: Windows loader initialization can run while a DLL is mapped, even when no @@ -40,8 +41,8 @@ export is invoked. ## Promise -- The contract and the PE reader are unit-tested without publishing anything - ([`tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests`](../CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md), +- The contract and the PE reader are unit-tested without publishing anything ([ + `tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests`](../CheatEngine.SDK.NativeAotLoaderHarness.Tests/README.md), Q41 at level C0): a `CEPlugin_*` export, a missing probe export and any unexpected export are refused. - The CI `aot` job's publish and harness transcript are the C2 evidence of Q41, recorded only after a green run. diff --git a/tests/CheatEngine.SDK.NativeAotLoaderHarness/packages.lock.json b/tests/CheatEngine.SDK.NativeAotLoaderHarness/packages.lock.json index 254741a9..b22b07c7 100644 --- a/tests/CheatEngine.SDK.NativeAotLoaderHarness/packages.lock.json +++ b/tests/CheatEngine.SDK.NativeAotLoaderHarness/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.QualificationTarget/README.md b/tests/CheatEngine.SDK.QualificationTarget/README.md index e6f3620c..d588526e 100644 --- a/tests/CheatEngine.SDK.QualificationTarget/README.md +++ b/tests/CheatEngine.SDK.QualificationTarget/README.md @@ -20,13 +20,13 @@ x86 .NET 6), so the target is published with Native AOT, which supports win-x86 ## How it works -| File | Content | -|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| File | Content | +|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `TargetLayout.cs` | The 16-byte module-resident marker (constant data of the image), the 8-byte many-results pattern (computed at run time, so it is not in the image), the initial values. | -| `TargetMemory.cs` | Pinned heap arrays: N marker copies 64 bytes apart, M back-to-back repetitions of the second pattern, and the Int32/Int64 cells. | -| `ImageScanner.cs` | Counts a pattern in every readable section of the executable image, or in a heap region. | -| `ReadyRecord.cs` | The one-line JSON records, written with `Utf8JsonWriter` (no reflection, trim-safe). | -| `Program.cs` | Arguments, the ready record, and the `step` / `exit` command loop on standard input. | +| `TargetMemory.cs` | Pinned heap arrays: N marker copies 64 bytes apart, M back-to-back repetitions of the second pattern, and the Int32/Int64 cells. | +| `ImageScanner.cs` | Counts a pattern in every readable section of the executable image, or in a heap region. | +| `ReadyRecord.cs` | The one-line JSON records, written with `Utf8JsonWriter` (no reflection, trim-safe). | +| `Program.cs` | Arguments, the ready record, and the `step` / `exit` command loop on standard input. | On start the target prints one line and waits for commands: @@ -44,20 +44,21 @@ compiler may emit a constant more than once, which is why the count is measured measured over their regions. Patterns that must be absent are chosen by the qualification driver, never stored in the target. -| Input | Effect | -|------------------------------|-----------------------------------------------------------------------------------------| -| `--heap-copies N` | Number of heap marker copies, 1–1024 (default 8). | -| `--repetitions M` | Number of many-results repetitions, 1–1000000 (default 20000). | -| `--ready-file PATH` | Also writes the ready record to PATH; the only file the target ever writes. | -| `step` on standard input | Adds one to both value cells and prints `{"event":"step","int32":…,"int64":…}`. | -| `exit` or end of input | Exits with code 0. | +| Input | Effect | +|--------------------------|---------------------------------------------------------------------------------| +| `--heap-copies N` | Number of heap marker copies, 1–1024 (default 8). | +| `--repetitions M` | Number of many-results repetitions, 1–1000000 (default 20000). | +| `--ready-file PATH` | Also writes the ready record to PATH; the only file the target ever writes. | +| `step` on standard input | Adds one to both value cells and prints `{"event":"step","int32":…,"int64":…}`. | +| `exit` or end of input | Exits with code 0. | The target opens no network connection, requests no elevation, reads no file and loads no SDK assembly. ## Promise - The target is in the solution, publishes with Native AOT for `win-x64` and `win-x86`, is not a test module and never - packs (`QualificationHarnessShapeTests.QualificationTarget_is_in_the_solution_and_publishes_native_aot_for_x64_and_x86`, + packs + (`QualificationHarnessShapeTests.QualificationTarget_is_in_the_solution_and_publishes_native_aot_for_x64_and_x86`, `QualificationHarnessShapeTests.Qualification_harnesses_are_not_test_modules_and_never_pack`). - It references no SDK project, so a qualification observes the SDK only from the plugin side (`QualificationHarnessShapeTests.QualificationTarget_references_no_SDK_project`). diff --git a/tests/CheatEngine.SDK.QualificationTarget/ReadyRecord.cs b/tests/CheatEngine.SDK.QualificationTarget/ReadyRecord.cs index c50ffe1b..ef049d44 100644 --- a/tests/CheatEngine.SDK.QualificationTarget/ReadyRecord.cs +++ b/tests/CheatEngine.SDK.QualificationTarget/ReadyRecord.cs @@ -30,7 +30,8 @@ internal static string Create(TargetMemory memory, nint imageBase, int imageSize Region(writer, "heap-marker", memory.HeapMarkersAddress, memory.HeapMarkers.Length, marker, ImageScanner.Count(memory.HeapMarkers, TargetLayout.Marker)); Region(writer, "many-results", memory.RepeatedAddress, memory.Repeated.Length, - TargetLayout.ToPattern(memory.RepeatedPattern), ImageScanner.Count(memory.Repeated, memory.RepeatedPattern)); + TargetLayout.ToPattern(memory.RepeatedPattern), + ImageScanner.Count(memory.Repeated, memory.RepeatedPattern)); writer.WriteEndArray(); writer.WriteStartArray("values"); Value(writer, "int32", memory.Int32Address, memory.Int32); diff --git a/tests/CheatEngine.SDK.QualificationTarget/TargetLayout.cs b/tests/CheatEngine.SDK.QualificationTarget/TargetLayout.cs index 530e7c5d..acc9bcd9 100644 --- a/tests/CheatEngine.SDK.QualificationTarget/TargetLayout.cs +++ b/tests/CheatEngine.SDK.QualificationTarget/TargetLayout.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text; namespace QualificationTarget; @@ -55,7 +56,7 @@ internal static string ToPattern(ReadOnlySpan bytes) builder.Append(' '); } - builder.Append(value.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + builder.Append(value.ToString("X2", CultureInfo.InvariantCulture)); } return builder.ToString(); diff --git a/tests/CheatEngine.SDK.QualificationTarget/TargetMemory.cs b/tests/CheatEngine.SDK.QualificationTarget/TargetMemory.cs index b7958f02..f8fff6de 100644 --- a/tests/CheatEngine.SDK.QualificationTarget/TargetMemory.cs +++ b/tests/CheatEngine.SDK.QualificationTarget/TargetMemory.cs @@ -19,20 +19,20 @@ internal TargetMemory(int heapCopies, int repetitions) Repetitions = repetitions; RepeatedPattern = TargetLayout.CreateRepeatedPattern(); - _heapMarkers = GC.AllocateArray(heapCopies * TargetLayout.HeapCopyStride, pinned: true); + _heapMarkers = GC.AllocateArray(heapCopies * TargetLayout.HeapCopyStride, true); for (int copy = 0; copy < heapCopies; copy++) { TargetLayout.Marker.CopyTo(_heapMarkers.AsSpan(copy * TargetLayout.HeapCopyStride)); } - _repeated = GC.AllocateArray(repetitions * TargetLayout.RepeatedPatternLength, pinned: true); + _repeated = GC.AllocateArray(repetitions * TargetLayout.RepeatedPatternLength, true); for (int repetition = 0; repetition < repetitions; repetition++) { RepeatedPattern.CopyTo(_repeated.AsSpan(repetition * TargetLayout.RepeatedPatternLength)); } // Slot 0 holds the Int32 cell in its low four bytes, slot 1 the Int64 cell; both stay naturally aligned. - _values = GC.AllocateArray(2, pinned: true); + _values = GC.AllocateArray(2, true); Int32 = TargetLayout.InitialInt32; Int64 = TargetLayout.InitialInt64; } diff --git a/tests/CheatEngine.SDK.QualificationTarget/packages.lock.json b/tests/CheatEngine.SDK.QualificationTarget/packages.lock.json index 12907abb..35f0a982 100644 --- a/tests/CheatEngine.SDK.QualificationTarget/packages.lock.json +++ b/tests/CheatEngine.SDK.QualificationTarget/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.DotNet.ILCompiler": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Repository.Tests/Abi/AbiRouteSeparationTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Abi/AbiRouteSeparationTests.cs index 11e06d4a..ab8eb00f 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Abi/AbiRouteSeparationTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Abi/AbiRouteSeparationTests.cs @@ -34,12 +34,14 @@ public void Managed_exports_are_never_converted_to_the_classic_prefix() { if (Identifier(Managed).IsMatch(code) && Identifier(Prefix).IsMatch(code)) { - problems.Add($"{file} names both {Managed} and {Prefix}: the managed and classic tables are separate routes."); + problems.Add( + $"{file} names both {Managed} and {Prefix}: the managed and classic tables are separate routes."); } foreach (Match cast in PrefixPointerCast().Matches(code)) { - problems.Add($"{file}:{CSharpCode.LineOf(code, cast.Index)} casts to {Prefix}*; read the prefix with ClassicExportedFunctionsPrefixReader."); + problems.Add( + $"{file}:{CSharpCode.LineOf(code, cast.Index)} casts to {Prefix}*; read the prefix with ClassicExportedFunctionsPrefixReader."); } } @@ -56,19 +58,22 @@ .. ProductionCode().Where(static source => Identifier(Prefix).IsMatch(source.Cod ]; Assert.All(consumers, file => Assert.Contains(file, PrefixConsumers, StringComparer.Ordinal)); - Assert.Contains("libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs", consumers, StringComparer.Ordinal); + Assert.Contains("libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs", consumers, + StringComparer.Ordinal); Assert.All(PrefixConsumers, file => Assert.True(RepositoryDocument.Exists(file), $"{file} no longer exists.")); - Assert.DoesNotContain(consumers, static file => file.StartsWith("libs/CheatEngine.SDK.Hosting/", StringComparison.Ordinal)); + Assert.DoesNotContain(consumers, + static file => file.StartsWith("libs/CheatEngine.SDK.Hosting/", StringComparison.Ordinal)); } [Fact] public void The_rules_see_code_and_ignore_comments_and_strings() { const string Documented = """ - /// Unlike ManagedExportedFunctions, the ExportedFunctionsPrefix is classic. - class C { string s = "ExportedFunctionsPrefix*"; } - """; - const string Converting = "unsafe class C { void M(ManagedExportedFunctions* m) { var p = (ExportedFunctionsPrefix*) m; } }"; + /// Unlike ManagedExportedFunctions, the ExportedFunctionsPrefix is classic. + class C { string s = "ExportedFunctionsPrefix*"; } + """; + const string Converting = + "unsafe class C { void M(ManagedExportedFunctions* m) { var p = (ExportedFunctionsPrefix*) m; } }"; string documented = CSharpCode.BlankCommentsAndLiterals(Documented); string converting = CSharpCode.BlankCommentsAndLiterals(Converting); @@ -92,13 +97,13 @@ private static Regex Identifier(string name) return string.Equals(name, Managed, StringComparison.Ordinal) ? ManagedIdentifier() : PrefixIdentifier(); } - [GeneratedRegex(@"\bManagedExportedFunctions\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\bManagedExportedFunctions\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex ManagedIdentifier(); - [GeneratedRegex(@"\bExportedFunctionsPrefix\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\bExportedFunctionsPrefix\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex PrefixIdentifier(); [GeneratedRegex(@"\(\s*(?:[\w.]+\.)?ExportedFunctionsPrefix\s*\*\s*\)", RegexOptions.CultureInvariant, - matchTimeoutMilliseconds: 1000)] + 1000)] private static partial Regex PrefixPointerCast(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryContract.cs b/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryContract.cs index 7b7a5a91..9bc9c628 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryContract.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryContract.cs @@ -7,7 +7,9 @@ namespace CheatEngine.SDK.Repository.Tests.Abi; /// internal static class ClassicSlotRegistryContract { - internal const string RegistryPath = "tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json"; + internal const string RegistryPath = + "tests/CheatEngine.SDK.Repository.Tests/Abi/TestData/classic-slot-registry.json"; + internal const string Kind = "cheatengine-classic-slot-registry/v0"; internal const string UpstreamCommit = "ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37"; internal const string QualifiableProfile = "ce-7.7.0.10621-x64-managed-hostfxr"; @@ -21,14 +23,16 @@ internal static class ClassicSlotRegistryContract internal static readonly string[] ContractRequired = [ - "pluginContractVersion", "hostTableType", "fieldCount", "pointerFieldCount", "x64TableSize", "sdkDirectPrefixSize", + "pluginContractVersion", "hostTableType", "fieldCount", "pointerFieldCount", "x64TableSize", + "sdkDirectPrefixSize", "sdkPrefixSlots" ]; internal static readonly string[] SlotRequired = [ "slot", "x64Offset", "width", "minDeclaredSize", "section", "hostField", "hostAssignment", "hostImplementation", - "nature", "indirection", "callingConvention", "nullability", "mirrors", "divergenceRefs", "sdkExposure", "sdkField", + "nature", "indirection", "callingConvention", "nullability", "mirrors", "divergenceRefs", "sdkExposure", + "sdkField", "facadeStatus", "ownership", "luaEquivalent", "catalogSurfaceId", "evidenceKind", "layoutEvidenceKind", "hostProfileStatus", "qualification" ]; @@ -41,11 +45,13 @@ internal static class ClassicSlotRegistryContract internal static readonly string[] CallbackCategoryRequired = [ - "pluginType", "name", "hostForm", "headerForm", "additionalForms", "versionVariants", "versionDependent", "context", + "pluginType", "name", "hostForm", "headerForm", "additionalForms", "versionVariants", "versionDependent", + "context", "synchronousDecision", "sdkRecord", "sdkCallbackTyped", "shapeTest", "note", "evidenceKind" ]; - internal static readonly string[] SourceIds = ["plugin-pas", "pluginexports-pas", "cepluginsdk-h", "cepluginsdk-pas"]; + internal static readonly string[] SourceIds = + ["plugin-pas", "pluginexports-pas", "cepluginsdk-h", "cepluginsdk-pas"]; internal static readonly string[] SourceRoles = ["Authority", "HostImplementation", "MirrorC", "MirrorPascal"]; @@ -53,7 +59,8 @@ internal static class ClassicSlotRegistryContract internal static readonly string[] Sections = ["Base", "V2", "V3", "V4", "V5"]; - internal static readonly string[] AssignmentKinds = ["Size", "FunctionAddress", "CellAddress", "VariableAddress", "Nil"]; + internal static readonly string[] AssignmentKinds = + ["Size", "FunctionAddress", "CellAddress", "VariableAddress", "Nil"]; internal static readonly string[] Natures = ["Int32", "Function", "FunctionPointerCell", "DataCell", "ObjectRefCell", "Unknown"]; diff --git a/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryDocumentTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryDocumentTests.cs index a1ca643d..13b3da97 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryDocumentTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Abi/ClassicSlotRegistryDocumentTests.cs @@ -26,7 +26,8 @@ public sealed partial class ClassicSlotRegistryDocumentTests .. Enumerable.Range(18, 10), .. Enumerable.Range(30, 6), .. Enumerable.Range(73, 9) ]; - private static readonly string[] AuditDivergences = ["D01", "D02", "D03", "D04", "D05", "D06", "D07", "D08", "D09", "D10"]; + private static readonly string[] AuditDivergences = + ["D01", "D02", "D03", "D04", "D05", "D06", "D07", "D08", "D09", "D10"]; private static JsonElement Registry => RepositoryDocument.LoadJson(ClassicSlotRegistryContract.RegistryPath); @@ -41,7 +42,8 @@ public void Registry_top_level_and_nested_objects_have_the_expected_required_pro AssertProperties(ClassicSlotRegistryContract.ContractRequired, root.GetProperty("contract")); AssertProperties(ClassicSlotRegistryContract.SlotRequired, Slots[0]); AssertProperties(ClassicSlotRegistryContract.DivergenceRequired, root.GetProperty("divergences")[0]); - AssertProperties(ClassicSlotRegistryContract.CallbackCategoryRequired, root.GetProperty("callbackCategories")[0]); + AssertProperties(ClassicSlotRegistryContract.CallbackCategoryRequired, + root.GetProperty("callbackCategories")[0]); Assert.All(root.GetProperty("sources").EnumerateArray(), source => AssertProperties(ClassicSlotRegistryContract.SourceRequired, source)); @@ -53,18 +55,28 @@ public void Registry_top_level_and_nested_objects_have_the_expected_required_pro Assert.Equal(ClassicSlotRegistryContract.Kind, root.GetProperty("schema").GetString()); Assert.All(root.GetProperty("sources").EnumerateArray(), source => - Assert.Contains(source.GetProperty("role").GetString(), ClassicSlotRegistryContract.SourceRoles, StringComparer.Ordinal)); + Assert.Contains(source.GetProperty("role").GetString(), ClassicSlotRegistryContract.SourceRoles, + StringComparer.Ordinal)); Assert.All(Slots, slot => { - Assert.Contains(slot.GetProperty("section").GetString(), ClassicSlotRegistryContract.Sections, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("hostAssignment").GetProperty("kind").GetString(), ClassicSlotRegistryContract.AssignmentKinds, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("nature").GetString(), ClassicSlotRegistryContract.Natures, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("callingConvention").GetString(), ClassicSlotRegistryContract.CallingConventions, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("nullability").GetString(), ClassicSlotRegistryContract.Nullabilities, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("sdkExposure").GetString(), ClassicSlotRegistryContract.SdkExposures, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("facadeStatus").GetString(), ClassicSlotRegistryContract.FacadeStatuses, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("ownership").GetString(), ClassicSlotRegistryContract.Ownerships, StringComparer.Ordinal); - Assert.Contains(slot.GetProperty("evidenceKind").GetString(), ClassicSlotRegistryContract.SlotEvidenceKinds, StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("section").GetString(), ClassicSlotRegistryContract.Sections, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("hostAssignment").GetProperty("kind").GetString(), + ClassicSlotRegistryContract.AssignmentKinds, StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("nature").GetString(), ClassicSlotRegistryContract.Natures, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("callingConvention").GetString(), + ClassicSlotRegistryContract.CallingConventions, StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("nullability").GetString(), ClassicSlotRegistryContract.Nullabilities, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("sdkExposure").GetString(), ClassicSlotRegistryContract.SdkExposures, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("facadeStatus").GetString(), ClassicSlotRegistryContract.FacadeStatuses, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("ownership").GetString(), ClassicSlotRegistryContract.Ownerships, + StringComparer.Ordinal); + Assert.Contains(slot.GetProperty("evidenceKind").GetString(), ClassicSlotRegistryContract.SlotEvidenceKinds, + StringComparer.Ordinal); }); } @@ -139,11 +151,14 @@ public void Sections_have_the_audit_slot_ranges() public void Nil_assigned_slots_are_exactly_the_eleven_host_nil_assignments() { int[] nil = SlotsWhere(static slot => IsKind(slot, "Nil")); - int[] nilAssigned = SlotsWhere(static slot => string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", StringComparison.Ordinal)); + int[] nilAssigned = SlotsWhere(static slot => + string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", StringComparison.Ordinal)); Assert.Equal(NilSlots, nil); Assert.Equal(NilSlots, nilAssigned); - Assert.All(nil, static index => Assert.Equal("nil", Slots[index].GetProperty("hostAssignment").GetProperty("expression").GetString())); + Assert.All(nil, + static index => Assert.Equal("nil", + Slots[index].GetProperty("hostAssignment").GetProperty("expression").GetString())); Assert.Equal("fixmem", HostName(Slots[14])); } @@ -152,8 +167,10 @@ public void Cell_slots_are_the_twenty_five_double_address_assignments() { int[] cells = SlotsWhere(static slot => IsKind(slot, "CellAddress")); int[] doubleAddress = SlotsWhere(static slot => - slot.GetProperty("hostAssignment").GetProperty("expression").GetString()!.StartsWith("@@", StringComparison.Ordinal)); - int[] functionPointerCells = SlotsWhere(static slot => string.Equals(slot.GetProperty("nature").GetString(), "FunctionPointerCell", StringComparison.Ordinal)); + slot.GetProperty("hostAssignment").GetProperty("expression").GetString()!.StartsWith("@@", + StringComparison.Ordinal)); + int[] functionPointerCells = SlotsWhere(static slot => string.Equals(slot.GetProperty("nature").GetString(), + "FunctionPointerCell", StringComparison.Ordinal)); Assert.Equal(25, cells.Length); Assert.Equal(CellSlots, cells); @@ -170,8 +187,12 @@ public void Cell_slots_are_the_twenty_five_double_address_assignments() [Fact] public void Data_and_object_reference_cells_have_their_declared_nature() { - Assert.Equal([4, 5], SlotsWhere(static slot => string.Equals(slot.GetProperty("nature").GetString(), "DataCell", StringComparison.Ordinal))); - Assert.Equal([82, 83], SlotsWhere(static slot => string.Equals(slot.GetProperty("nature").GetString(), "ObjectRefCell", StringComparison.Ordinal))); + Assert.Equal([4, 5], + SlotsWhere(static slot => + string.Equals(slot.GetProperty("nature").GetString(), "DataCell", StringComparison.Ordinal))); + Assert.Equal([82, 83], + SlotsWhere(static slot => + string.Equals(slot.GetProperty("nature").GetString(), "ObjectRefCell", StringComparison.Ordinal))); int[] cells = [4, 5, 82, 83]; foreach (int index in cells) { @@ -197,8 +218,12 @@ public void Data_and_object_reference_cells_have_their_declared_nature() public void Divergence_register_lists_the_ten_audit_divergences() { JsonElement[] divergences = [.. Registry.GetProperty("divergences").EnumerateArray()]; - string[] audit = [.. divergences.Where(static row => string.Equals(row.GetProperty("origin").GetString(), "Audit", StringComparison.Ordinal)) - .Select(static row => row.GetProperty("id").GetString()!)]; + string[] audit = + [ + .. divergences.Where(static row => + string.Equals(row.GetProperty("origin").GetString(), "Audit", StringComparison.Ordinal)) + .Select(static row => row.GetProperty("id").GetString()!) + ]; Assert.Equal(AuditDivergences, audit); Assert.Equal([17], Ints(Divergence(divergences, "D01"), "slots")); @@ -213,7 +238,8 @@ public void Divergence_register_lists_the_ten_audit_divergences() Assert.Contains("PluginInitRecord", Strings(Divergence(divergences, "D10"), "records"), StringComparer.Ordinal); // Lot observations are extra rows, never replacements: each is a source observation of the pinned source. - foreach (JsonElement row in divergences.Where(static row => string.Equals(row.GetProperty("origin").GetString(), "LotObservation", StringComparison.Ordinal))) + foreach (JsonElement row in divergences.Where(static row => + string.Equals(row.GetProperty("origin").GetString(), "LotObservation", StringComparison.Ordinal))) { Assert.Equal("ObservedSource", row.GetProperty("evidenceKind").GetString()); } @@ -232,14 +258,20 @@ public void Divergence_references_resolve_in_both_directions() foreach (JsonElement slot in Slots) { int index = slot.GetProperty("slot").GetInt32(); - string[] refs = [.. slot.GetProperty("divergenceRefs").EnumerateArray().Select(static id => id.GetString()!)]; + string[] refs = + [.. slot.GetProperty("divergenceRefs").EnumerateArray().Select(static id => id.GetString()!)]; foreach (string id in refs) { - Assert.True(slotsById.TryGetValue(id, out int[]? listed), $"Slot {index} cites the unknown divergence {id}."); + Assert.True(slotsById.TryGetValue(id, out int[]? listed), + $"Slot {index} cites the unknown divergence {id}."); Assert.Contains(index, listed); } - string[] expected = [.. slotsById.Where(pair => pair.Value.Contains(index)).Select(static pair => pair.Key).Order(StringComparer.Ordinal)]; + string[] expected = + [ + .. slotsById.Where(pair => pair.Value.Contains(index)).Select(static pair => pair.Key) + .Order(StringComparer.Ordinal) + ]; Assert.Equal(expected, refs.Order(StringComparer.Ordinal), StringComparer.Ordinal); } @@ -255,18 +287,23 @@ public void Every_divergent_or_nil_slot_is_never_prefix_typed() foreach (JsonElement slot in Slots) { bool divergent = slot.GetProperty("divergenceRefs").GetArrayLength() > 0; - bool nil = string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", StringComparison.Ordinal); + bool nil = string.Equals(slot.GetProperty("nullability").GetString(), "NilAssigned", + StringComparison.Ordinal); if (divergent || nil) { Assert.NotEqual("PrefixTyped", slot.GetProperty("sdkExposure").GetString(), StringComparer.Ordinal); } } - Assert.Equal([14, 17], SlotsWhere(static slot => string.Equals(slot.GetProperty("sdkExposure").GetString(), "PrefixOpaque", StringComparison.Ordinal))); + Assert.Equal([14, 17], + SlotsWhere(static slot => string.Equals(slot.GetProperty("sdkExposure").GetString(), "PrefixOpaque", + StringComparison.Ordinal))); Assert.Equal([.. Enumerable.Range(0, 18).Except([14, 17])], - SlotsWhere(static slot => string.Equals(slot.GetProperty("sdkExposure").GetString(), "PrefixTyped", StringComparison.Ordinal))); + SlotsWhere(static slot => string.Equals(slot.GetProperty("sdkExposure").GetString(), "PrefixTyped", + StringComparison.Ordinal))); Assert.Equal([.. Enumerable.Range(18, 141)], - SlotsWhere(static slot => string.Equals(slot.GetProperty("sdkExposure").GetString(), "None", StringComparison.Ordinal))); + SlotsWhere(static slot => + string.Equals(slot.GetProperty("sdkExposure").GetString(), "None", StringComparison.Ordinal))); Assert.All(Slots[18..], static slot => { Assert.Equal("Deferred", slot.GetProperty("facadeStatus").GetString()); @@ -279,9 +316,12 @@ public void Authority_is_plugin_pas_and_mirrors_carry_their_hashes() { JsonElement[] sources = [.. Registry.GetProperty("sources").EnumerateArray()]; - Assert.Equal(ClassicSlotRegistryContract.SourceIds, sources.Select(static source => source.GetProperty("id").GetString()!), + Assert.Equal(ClassicSlotRegistryContract.SourceIds, + sources.Select(static source => source.GetProperty("id").GetString()!), StringComparer.Ordinal); - JsonElement authority = Assert.Single(sources, static source => string.Equals(source.GetProperty("role").GetString(), "Authority", StringComparison.Ordinal)); + JsonElement authority = Assert.Single(sources, + static source => string.Equals(source.GetProperty("role").GetString(), "Authority", + StringComparison.Ordinal)); Assert.Equal("Cheat Engine/plugin.pas", authority.GetProperty("path").GetString()); foreach (JsonElement source in sources) { @@ -293,7 +333,8 @@ public void Authority_is_plugin_pas_and_mirrors_carry_their_hashes() JsonElement header = sources[2].GetProperty("installed"); Assert.Equal("MirrorC", sources[2].GetProperty("role").GetString()); - Assert.Equal("9c0e31bb753d782ce20710d19828f4e97b4371c8733abd0c5c6f7f485306fb28", header.GetProperty("sha256").GetString()); + Assert.Equal("9c0e31bb753d782ce20710d19828f4e97b4371c8733abd0c5c6f7f485306fb28", + header.GetProperty("sha256").GetString()); Assert.Equal("CommentOnlyDifference", header.GetProperty("relation").GetString()); JsonElement pascal = sources[3].GetProperty("installed"); Assert.Equal("MirrorPascal", sources[3].GetProperty("role").GetString()); @@ -310,14 +351,16 @@ public void Authority_is_plugin_pas_and_mirrors_carry_their_hashes() }); // A host implementation is only cited for a function address whose target plugin.pas assigns. - Assert.All(Slots.Where(static slot => slot.GetProperty("hostImplementation").ValueKind != JsonValueKind.Null), static slot => - { - Assert.Equal("FunctionAddress", AssignmentKind(slot)); - Assert.Equal("pluginexports-pas", slot.GetProperty("hostImplementation").GetProperty("source").GetString()); - Assert.Equal("ObservedSource", slot.GetProperty("evidenceKind").GetString()); - }); + Assert.All(Slots.Where(static slot => slot.GetProperty("hostImplementation").ValueKind != JsonValueKind.Null), + static slot => + { + Assert.Equal("FunctionAddress", AssignmentKind(slot)); + Assert.Equal("pluginexports-pas", + slot.GetProperty("hostImplementation").GetProperty("source").GetString()); + Assert.Equal("ObservedSource", slot.GetProperty("evidenceKind").GetString()); + }); Assert.All(Slots.Where(static slot => IsKind(slot, "FunctionAddress") && - slot.GetProperty("hostImplementation").ValueKind == JsonValueKind.Null), + slot.GetProperty("hostImplementation").ValueKind == JsonValueKind.Null), static slot => Assert.Equal("ToQualify", slot.GetProperty("evidenceKind").GetString())); } @@ -327,14 +370,17 @@ public void Qualifiable_profile_status_is_not_observed_with_a_reason() JsonElement[] statuses = [.. Registry.GetProperty("profileStatus").EnumerateArray()]; JsonElement qualifiable = Assert.Single(statuses, static status => - string.Equals(status.GetProperty("profileId").GetString(), ClassicSlotRegistryContract.QualifiableProfile, StringComparison.Ordinal)); + string.Equals(status.GetProperty("profileId").GetString(), ClassicSlotRegistryContract.QualifiableProfile, + StringComparison.Ordinal)); Assert.Equal("NotObserved", qualifiable.GetProperty("status").GetString()); Assert.Contains("no managed-hostfxr route reaches the classic table", qualifiable.GetProperty("reason").GetString(), StringComparison.Ordinal); JsonElement documentary = Assert.Single(statuses, static status => - string.Equals(status.GetProperty("profileId").GetString(), ClassicSlotRegistryContract.DocumentaryProfile, StringComparison.Ordinal)); + string.Equals(status.GetProperty("profileId").GetString(), ClassicSlotRegistryContract.DocumentaryProfile, + StringComparison.Ordinal)); Assert.Equal("SourceOnly", documentary.GetProperty("status").GetString()); - Assert.All(Slots, static slot => Assert.Equal("NotObserved", slot.GetProperty("hostProfileStatus").GetString())); + Assert.All(Slots, + static slot => Assert.Equal("NotObserved", slot.GetProperty("hostProfileStatus").GetString())); } [Fact] @@ -351,7 +397,8 @@ public void No_slot_is_counted_as_qualified() [Fact] public void Lua_equivalents_are_named_only_for_function_slots() { - JsonElement[] withLua = [.. Slots.Where(static slot => slot.GetProperty("luaEquivalent").ValueKind != JsonValueKind.Null)]; + JsonElement[] withLua = + [.. Slots.Where(static slot => slot.GetProperty("luaEquivalent").ValueKind != JsonValueKind.Null)]; Assert.NotEmpty(withLua); Assert.All(withLua, static slot => @@ -360,7 +407,8 @@ public void Lua_equivalents_are_named_only_for_function_slots() Assert.Equal(HostName(slot), slot.GetProperty("luaEquivalent").GetProperty("name").GetString(), StringComparer.OrdinalIgnoreCase); }); - Assert.All(Slots, static slot => Assert.Equal(JsonValueKind.Null, slot.GetProperty("catalogSurfaceId").ValueKind)); + Assert.All(Slots, + static slot => Assert.Equal(JsonValueKind.Null, slot.GetProperty("catalogSurfaceId").ValueKind)); Assert.Equal("createForm", Slots[124].GetProperty("luaEquivalent").GetProperty("name").GetString()); } @@ -393,10 +441,14 @@ public void Callback_categories_cover_plugin_types_0_to_8() { JsonElement[] categories = [.. Registry.GetProperty("callbackCategories").EnumerateArray()]; - Assert.Equal([.. Enumerable.Range(0, 9)], categories.Select(static row => row.GetProperty("pluginType").GetInt32())); + Assert.Equal([.. Enumerable.Range(0, 9)], + categories.Select(static row => row.GetProperty("pluginType").GetInt32())); Assert.Equal( - ["ptAddressList", "ptMemoryView", "ptOnDebugEvent", "ptProcesswatcherEvent", "ptFunctionPointerchange", "ptMainMenu", - "ptDisassemblerContext", "ptDisassemblerRenderLine", "ptAutoAssembler"], + [ + "ptAddressList", "ptMemoryView", "ptOnDebugEvent", "ptProcesswatcherEvent", "ptFunctionPointerchange", + "ptMainMenu", + "ptDisassemblerContext", "ptDisassemblerRenderLine", "ptAutoAssembler" + ], categories.Select(static row => row.GetProperty("name").GetString()!), StringComparer.Ordinal); Assert.True(categories[2].GetProperty("synchronousDecision").GetBoolean()); Assert.Equal(1, categories.Count(static row => row.GetProperty("synchronousDecision").GetBoolean())); @@ -449,7 +501,8 @@ private static string HostName(JsonElement slot) private static JsonElement Divergence(JsonElement[] divergences, string id) { - return Assert.Single(divergences, row => string.Equals(row.GetProperty("id").GetString(), id, StringComparison.Ordinal)); + return Assert.Single(divergences, + row => string.Equals(row.GetProperty("id").GetString(), id, StringComparison.Ordinal)); } private static int[] Ints(JsonElement row, string property) @@ -464,10 +517,12 @@ private static string[] Strings(JsonElement row, string property) private static bool InRange(JsonElement[] ranges, int line) { - return ranges.Any(range => line >= range.GetProperty("start").GetInt32() && line <= range.GetProperty("end").GetInt32()); + return ranges.Any(range => + line >= range.GetProperty("start").GetInt32() && line <= range.GetProperty("end").GetInt32()); } - [GeneratedRegex(@"(?ProjectReference to. This project has none, by design (its own top-of-file /// comment: it only reads committed files and never builds, packs or restores), so it cannot reflect over a /// referenced assembly. Instead it loads the shipping assemblies' own build output with -/// from artifacts/bin/CheatEngine.SDK/<configuration> — the one +/// from artifacts/bin/CheatEngine.SDK/<configuration> — the one /// folder src/CheatEngine.SDK copies every libs/ assembly into (its csproj comment "Libraries embedded /// under lib/net10.0") alongside its own — which the solution build that runs before this test module (shared /// contracts section 1.8: build, then pack, then test) has already populated. This is still a from-disk load, never @@ -27,7 +27,16 @@ namespace CheatEngine.SDK.Repository.Tests.ApiGates; /// public sealed class ApiGateDiagnosticTests { - private const string UrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; + private const string UrlFormat = + "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; + + /// + /// The shipping assemblies (libs/ + src/CheatEngine.SDK itself), loaded once per test process from + /// the current configuration's build output. The configuration is read from this test module's own output + /// directory name (artifacts/bin/CheatEngine.SDK.Repository.Tests/<configuration>), never + /// hard-coded, so the same test scans whichever leg (Debug or Release) built it. + /// + private static readonly Lazy> s_shippingAssemblies = new(LoadShippingAssemblies); [Fact] public void Every_api_gate_attribute_uses_a_CESDK_id_and_the_documentation_url_format() @@ -44,7 +53,8 @@ public void Every_api_gate_attribute_uses_a_CESDK_id_and_the_documentation_url_f if (!string.Equals(gate.UrlFormat, UrlFormat, StringComparison.Ordinal)) { - offenders.Add($"{gate.Location}: {gate.Kind}('{gate.Id}') has UrlFormat '{gate.UrlFormat}', expected '{UrlFormat}'."); + offenders.Add( + $"{gate.Location}: {gate.Kind}('{gate.Id}') has UrlFormat '{gate.UrlFormat}', expected '{UrlFormat}'."); } } @@ -99,14 +109,6 @@ private static bool HasExpectedShape(string id, ApiGateKind kind) return true; } - /// - /// The shipping assemblies (libs/ + src/CheatEngine.SDK itself), loaded once per test process from - /// the current configuration's build output. The configuration is read from this test module's own output - /// directory name (artifacts/bin/CheatEngine.SDK.Repository.Tests/<configuration>), never - /// hard-coded, so the same test scans whichever leg (Debug or Release) built it. - /// - private static readonly Lazy> s_shippingAssemblies = new(LoadShippingAssemblies); - private static List ApiGates() { List gates = []; @@ -156,7 +158,8 @@ private static List LoadShippingAssemblies() } List assemblies = []; - foreach (string dll in Directory.EnumerateFiles(directory, "CheatEngine.SDK*.dll", SearchOption.TopDirectoryOnly)) + foreach (string dll in Directory.EnumerateFiles(directory, "CheatEngine.SDK*.dll", + SearchOption.TopDirectoryOnly)) { assemblies.Add(Assembly.LoadFrom(dll)); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Diagnostics/ApiDiagnosticIdTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Diagnostics/ApiDiagnosticIdTests.cs index 3310be7d..7f52ab7b 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Diagnostics/ApiDiagnosticIdTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Diagnostics/ApiDiagnosticIdTests.cs @@ -6,7 +6,8 @@ namespace CheatEngine.SDK.Repository.Tests.Diagnostics; /// /// The diagnostic identifiers that SDK product code declares on its own API through -/// [Obsolete(DiagnosticId = "CESDKnnnn")] and [Experimental("CESDKnnnn")]. The C# compiler raises them in +/// [Obsolete(DiagnosticId = "CESDKnnnn")] and [Experimental("CESDKnnnn")]. The C# compiler raises them +/// in /// consumer code, so, like the analyzer descriptors that DiagnosticCatalogTests covers, each one needs a /// documentation page, a row in analyzers/docs/README.md, the repository help-link format and an identifier /// from its reserved range (shared-contracts section 3.2: 5xxx experimental gates, 7xxx obsoletions). @@ -22,16 +23,15 @@ namespace CheatEngine.SDK.Repository.Tests.Diagnostics; /// public sealed partial class ApiDiagnosticIdTests { - private const string HelpUrlFormat = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; + private const string HelpUrlFormat = + "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/{0}.md"; /// /// Reviewed obsoletions outside the 7xxx range: the legacy Lua registration pair shares its usage analyzer /// identifier (shared-contracts section 3.3, S-REG-ANALYZER, CESDK1006). /// - private static readonly HashSet s_reviewedObsoleteIdsOutsideTheRange = new(StringComparer.Ordinal) - { - "CESDK1006" - }; + private static readonly HashSet s_reviewedObsoleteIdsOutsideTheRange = + new(StringComparer.Ordinal) { "CESDK1006" }; [Fact] public void the_source_scan_finds_the_declared_api_diagnostic_ids() @@ -39,7 +39,8 @@ public void the_source_scan_finds_the_declared_api_diagnostic_ids() IReadOnlyList ids = ScanApiDiagnosticIds(); Assert.Contains(ids, static id => id is { Kind: ApiDiagnosticKind.Obsolete, Id: "CESDK7001" } && - string.Equals(id.File, "libs/CheatEngine.SDK.Engine/Runtime/PointerSize.cs", StringComparison.Ordinal)); + string.Equals(id.File, "libs/CheatEngine.SDK.Engine/Runtime/PointerSize.cs", + StringComparison.Ordinal)); } [Fact] @@ -97,7 +98,8 @@ public void every_obsolete_or_experimental_diagnostic_id_uses_the_repository_hel { if (!string.Equals(id.UrlFormat, HelpUrlFormat, StringComparison.Ordinal)) { - offenders.Add($"{id.File}: {id.Id} has UrlFormat '{id.UrlFormat ?? "(none)"}', expected '{HelpUrlFormat}'."); + offenders.Add( + $"{id.File}: {id.Id} has UrlFormat '{id.UrlFormat ?? "(none)"}', expected '{HelpUrlFormat}'."); } } @@ -147,7 +149,8 @@ public static void D() { } List ids = [.. Scan("sample.cs", Source)]; Assert.Equal(3, ids.Count); - Assert.Equal(new ApiDiagnosticId("sample.cs", ApiDiagnosticKind.Obsolete, "CESDK7999", "https://example.invalid/{0}"), + Assert.Equal( + new ApiDiagnosticId("sample.cs", ApiDiagnosticKind.Obsolete, "CESDK7999", "https://example.invalid/{0}"), ids[0]); Assert.Equal(new ApiDiagnosticId("sample.cs", ApiDiagnosticKind.Experimental, "CESDK5999", "https://example.invalid/{0}.md"), ids[1]); @@ -224,9 +227,9 @@ public void the_scan_reads_attribute_lists_and_targets_and_reports_identifiers_t new("sample.cs", ApiDiagnosticKind.Experimental, "CESDK5908", null), new("sample.cs", ApiDiagnosticKind.Experimental, "CESDK5909", null), new("sample.cs", ApiDiagnosticKind.Obsolete, "CESDK7911", "c"), - new("sample.cs", ApiDiagnosticKind.Obsolete, "Ids.Old", "d", IsLiteral: false), - new("sample.cs", ApiDiagnosticKind.Experimental, "Ids.Gate", null, IsLiteral: false), - new("sample.cs", ApiDiagnosticKind.Obsolete, "\"SYSLIB0999\"", null, IsLiteral: false) + new("sample.cs", ApiDiagnosticKind.Obsolete, "Ids.Old", "d", false), + new("sample.cs", ApiDiagnosticKind.Experimental, "Ids.Gate", null, false), + new("sample.cs", ApiDiagnosticKind.Obsolete, "\"SYSLIB0999\"", null, false) ]; Assert.Equal(expected, Scan("sample.cs", Source)); @@ -237,7 +240,8 @@ private static List ScanApiDiagnosticIds() List ids = []; foreach (string file in RepositoryRoot.EnumerateSourceFiles("*.cs")) { - if (!file.StartsWith("libs/", StringComparison.Ordinal) && !file.StartsWith("src/", StringComparison.Ordinal)) + if (!file.StartsWith("libs/", StringComparison.Ordinal) && + !file.StartsWith("src/", StringComparison.Ordinal)) { continue; } @@ -274,7 +278,7 @@ private static IEnumerable Scan(string file, string source) if (expression.Success) { yield return new ApiDiagnosticId(file, kind, expression.Groups["expression"].Value.Trim(), urlFormat, - IsLiteral: false); + false); } } } @@ -378,7 +382,7 @@ private void ReadCode(bool inHole) { depth++; } - else if ((current is ')' or ']' or '}') && depth > 0) + else if (current is ')' or ']' or '}' && depth > 0) { depth--; } @@ -408,7 +412,7 @@ private bool TryReadStringLiteral() int index = _position; int dollars = 0; bool verbatim = false; - while (index < _source.Length && (_source[index] is '$' or '@')) + while (index < _source.Length && _source[index] is '$' or '@') { if (_source[index] == '$') { diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/DependabotConfigurationTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/DependabotConfigurationTests.cs index faeae296..26f4eaeb 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/DependabotConfigurationTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/DependabotConfigurationTests.cs @@ -7,7 +7,8 @@ namespace CheatEngine.SDK.Repository.Tests.Governance; /// /// .github/dependabot.yml (audit register PR-CQ-07): every ecosystem waits before proposing a fresh release, /// the Roslyn pin and the SDK-implicit packages never move on their own, and titles stay compatible with the pull -/// request policy. https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference +/// request policy. +/// https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference /// public sealed class DependabotConfigurationTests { @@ -50,7 +51,9 @@ public void Every_ecosystem_has_a_cooldown_of_at_least_seven_days() foreach (string key in s_cooldownKeys) { string? value = YamlDocument.Scalar(cooldown, key); - if (value is not null && (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out int days) || days < MinimumCooldownDays)) + if (value is not null && + (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out int days) || + days < MinimumCooldownDays)) { problems.Add($"{ecosystem}: {key} is {value}"); } @@ -69,7 +72,8 @@ public void Roslyn_pins_and_sdk_implicit_packages_are_ignored() foreach (YamlMappingNode rule in YamlDocument.Mappings(nuget, "ignore")) { // A whole-package ignore has no update-types: a partial ignore would still let some bumps through. - Assert.True(YamlDocument.Child(rule, "update-types") is null && YamlDocument.Child(rule, "versions") is null, + Assert.True( + YamlDocument.Child(rule, "update-types") is null && YamlDocument.Child(rule, "versions") is null, $"The nuget ignore rule for '{YamlDocument.Scalar(rule, "dependency-name")}' must ignore every version."); ignored.Add(YamlDocument.Scalar(rule, "dependency-name") ?? ""); } @@ -116,13 +120,15 @@ public void Dotnet_sdk_ecosystem_ignores_major_updates() foreach (YamlMappingNode rule in YamlDocument.Mappings(sdk, "ignore")) { if (string.Equals(YamlDocument.Scalar(rule, "dependency-name"), "*", StringComparison.Ordinal) - && YamlDocument.Scalars(rule, "update-types").Contains("version-update:semver-major", StringComparer.Ordinal)) + && YamlDocument.Scalars(rule, "update-types") + .Contains("version-update:semver-major", StringComparer.Ordinal)) { ignoresMajor = true; } } - Assert.True(ignoresMajor, "The dotnet-sdk ecosystem must ignore semver-major updates: a new .NET major is a migration."); + Assert.True(ignoresMajor, + "The dotnet-sdk ecosystem must ignore semver-major updates: a new .NET major is a migration."); } [Fact] @@ -172,7 +178,8 @@ public void Specific_nuget_groups_come_before_the_catch_all_group() private static List Updates() { - IReadOnlyList updates = YamlDocument.Mappings(YamlDocument.Load(ConfigurationPath).Root, "updates"); + IReadOnlyList updates = + YamlDocument.Mappings(YamlDocument.Load(ConfigurationPath).Root, "updates"); Assert.NotEmpty(updates); return [.. updates]; } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceDocumentTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceDocumentTests.cs index eceeb38c..d3e9f64d 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceDocumentTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceDocumentTests.cs @@ -19,9 +19,14 @@ public sealed partial class GovernanceDocumentTests private const string IssueTemplates = ".github/ISSUE_TEMPLATE"; private const string CompatibilityForm = ".github/ISSUE_TEMPLATE/compatibility.yml"; private const string ChooserConfiguration = ".github/ISSUE_TEMPLATE/config.yml"; - private const string PrivateReportingUrl = "https://github.com/CheatEngineNet/CheatEngine.SDK/security/advisories/new"; + + private const string PrivateReportingUrl = + "https://github.com/CheatEngineNet/CheatEngine.SDK/security/advisories/new"; + private const string LuaFixture = "native/cheat-engine/lua53-64.dll"; - private const string BridgeBinary = "native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll"; + + private const string BridgeBinary = + "native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll"; /// Maintainers who may own paths; CODEOWNERS is informational (no required code-owner review). private static readonly string[] s_knownOwners = ["@AriusII", "@ShadowNineX"]; @@ -42,7 +47,10 @@ public void Security_policy_names_private_reporting_scope_response_and_supported string text = RepositoryFile.ReadText(SecurityPolicy); Assert.Equal( - ["Supported versions", "Reporting a vulnerability", "Scope", "Response", "Verifying releases", "Binary files in this repository"], + [ + "Supported versions", "Reporting a vulnerability", "Scope", "Response", "Verifying releases", + "Binary files in this repository" + ], Level2Headings(text)); Assert.Contains(PrivateReportingUrl, text, StringComparison.Ordinal); Assert.Contains("Never report a vulnerability in a public issue", text, StringComparison.Ordinal); @@ -67,7 +75,8 @@ public void Security_policy_describes_the_committed_binaries_with_their_real_has string actual = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(RepositoryFile.FullPath(LuaFixture)))); Assert.Contains($"`{actual}`", policy, StringComparison.Ordinal); - Assert.Contains($"`{actual}`", RepositoryFile.ReadText("native/cheat-engine/README.md"), StringComparison.Ordinal); + Assert.Contains($"`{actual}`", RepositoryFile.ReadText("native/cheat-engine/README.md"), + StringComparison.Ordinal); } [Fact] @@ -75,7 +84,8 @@ public void Code_of_conduct_routes_reports_through_private_reporting_without_an_ { string text = RepositoryFile.ReadText(CodeOfConduct); - Assert.Contains("https://www.contributor-covenant.org/version/2/1/code_of_conduct.html", text, StringComparison.Ordinal); + Assert.Contains("https://www.contributor-covenant.org/version/2/1/code_of_conduct.html", text, + StringComparison.Ordinal); Assert.Contains(PrivateReportingUrl, text, StringComparison.Ordinal); Assert.DoesNotMatch(EmailAddress(), text); } @@ -103,7 +113,8 @@ public void Codeowners_patterns_point_to_existing_paths_and_known_owners() Assert.NotEmpty(owners); foreach (string owner in owners) { - Assert.True(Array.IndexOf(s_knownOwners, owner) >= 0, $"{CodeOwners}: '{owner}' is not a known maintainer."); + Assert.True(Array.IndexOf(s_knownOwners, owner) >= 0, + $"{CodeOwners}: '{owner}' is not a known maintainer."); } // GitHub rejects "!" negation and "[ ]" ranges in CODEOWNERS. @@ -116,7 +127,8 @@ public void Codeowners_patterns_point_to_existing_paths_and_known_owners() string path = pattern.Trim('/'); Assert.True(RepositoryFile.ExistsWithExactCase(path, out bool isDirectory), $"{CodeOwners}: '{pattern}' matches nothing in the repository (exact case)."); - Assert.True(!pattern.EndsWith('/') || isDirectory, $"{CodeOwners}: '{pattern}' names a folder that is a file."); + Assert.True(!pattern.EndsWith('/') || isDirectory, + $"{CodeOwners}: '{pattern}' names a folder that is a file."); } } @@ -136,24 +148,29 @@ public void Compatibility_issue_form_requires_the_full_tuple() foreach (string id in s_requiredTupleIds) { - Assert.True(elements.TryGetValue(id, out YamlMappingNode? element), $"{CompatibilityForm} has no '{id}' field."); + Assert.True(elements.TryGetValue(id, out YamlMappingNode? element), + $"{CompatibilityForm} has no '{id}' field."); Assert.True(IsRequired(element), $"{CompatibilityForm}: '{id}' must be required."); } foreach (string id in s_optionalTupleIds) { - Assert.True(elements.TryGetValue(id, out YamlMappingNode? element), $"{CompatibilityForm} has no '{id}' field."); + Assert.True(elements.TryGetValue(id, out YamlMappingNode? element), + $"{CompatibilityForm} has no '{id}' field."); Assert.False(IsRequired(element), $"{CompatibilityForm}: '{id}' is optional."); } - IReadOnlyList levels = YamlDocument.Scalars(YamlDocument.Child(elements["evidence-level"], "attributes"), "options"); + IReadOnlyList levels = + YamlDocument.Scalars(YamlDocument.Child(elements["evidence-level"], "attributes"), "options"); foreach (string level in (string[]) ["(C0)", "(C1/C2)", "(C3)", "(C4)"]) { Assert.Contains(levels, option => option.EndsWith(level, StringComparison.Ordinal)); } - IReadOnlyList profiles = YamlDocument.Scalars(YamlDocument.Child(elements["load-profile"], "attributes"), "options"); - Assert.Contains(profiles, static option => option.Contains("ce-7.7.0.10621-x64-managed-hostfxr", StringComparison.Ordinal)); + IReadOnlyList profiles = + YamlDocument.Scalars(YamlDocument.Child(elements["load-profile"], "attributes"), "options"); + Assert.Contains(profiles, + static option => option.Contains("ce-7.7.0.10621-x64-managed-hostfxr", StringComparison.Ordinal)); } [Fact] @@ -193,17 +210,20 @@ public void Issue_form_element_ids_are_unique_and_valid() foreach (YamlMappingNode element in body) { string type = YamlDocument.Scalar(element, "type") ?? ""; - Assert.Contains(type, (string[]) ["markdown", "input", "textarea", "dropdown", "checkboxes"], StringComparer.Ordinal); + Assert.Contains(type, (string[]) ["markdown", "input", "textarea", "dropdown", "checkboxes"], + StringComparer.Ordinal); string? id = YamlDocument.Scalar(element, "id"); if (string.Equals(type, "markdown", StringComparison.Ordinal)) { continue; } - Assert.True(id is not null && ElementId().IsMatch(id), $"{path}: a {type} element has no valid id ('{id}')."); + Assert.True(id is not null && ElementId().IsMatch(id), + $"{path}: a {type} element has no valid id ('{id}')."); Assert.True(ids.Add(id), $"{path}: the id '{id}' is used twice."); YamlNode? attributes = YamlDocument.Child(element, "attributes"); - Assert.False(string.IsNullOrWhiteSpace(YamlDocument.Scalar(attributes, "label")), $"{path}: '{id}' has no label."); + Assert.False(string.IsNullOrWhiteSpace(YamlDocument.Scalar(attributes, "label")), + $"{path}: '{id}' has no label."); if (string.Equals(type, "dropdown", StringComparison.Ordinal)) { IReadOnlyList options = YamlDocument.Scalars(attributes, "options"); @@ -216,7 +236,8 @@ public void Issue_form_element_ids_are_unique_and_valid() Assert.DoesNotMatch(AbsoluteLocalPath(), form.Text); foreach (Match link in SelfLink().Matches(form.Text)) { - Assert.True(TryCheckSelfLink(link, out string? problem), $"{path}: {link.Value} does not resolve ({problem})."); + Assert.True(TryCheckSelfLink(link, out string? problem), + $"{path}: {link.Value} does not resolve ({problem})."); } } } @@ -232,7 +253,8 @@ public void Issue_forms_disable_blank_issues_and_link_private_reporting() { foreach (string key in (string[]) ["name", "url", "about"]) { - Assert.False(string.IsNullOrWhiteSpace(YamlDocument.Scalar(link, key)), $"{ChooserConfiguration}: a contact link has no {key}."); + Assert.False(string.IsNullOrWhiteSpace(YamlDocument.Scalar(link, key)), + $"{ChooserConfiguration}: a contact link has no {key}."); } string url = YamlDocument.Scalar(link, "url")!; @@ -266,7 +288,8 @@ public void Issue_forms_never_instruct_editing_the_cheat_engine_runtime_configur } } - Assert.True(offenders.Count == 0, $"Issue forms must never ask users to edit ce.runtimeconfig.json: {string.Join("; ", offenders)}"); + Assert.True(offenders.Count == 0, + $"Issue forms must never ask users to edit ce.runtimeconfig.json: {string.Join("; ", offenders)}"); } /// The ## (ATX level-2) headings of a Markdown document, outside fenced code blocks, in order. @@ -278,7 +301,8 @@ private static List Level2Headings(string text) { string line = rawLine.TrimEnd(); string trimmedStart = line.TrimStart(); - if (trimmedStart.StartsWith("```", StringComparison.Ordinal) || trimmedStart.StartsWith("~~~", StringComparison.Ordinal)) + if (trimmedStart.StartsWith("```", StringComparison.Ordinal) || + trimmedStart.StartsWith("~~~", StringComparison.Ordinal)) { inFence = !inFence; continue; @@ -376,33 +400,37 @@ private static IEnumerable Sentences(string text) } } - [GeneratedRegex(@"(?<=[.!?;])\s+|\r?\n", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"(?<=[.!?;])\s+|\r?\n", RegexOptions.CultureInvariant, 1000)] private static partial Regex SentenceBoundary(); - [GeneratedRegex(@"\b(edit|modify|change|replace|overwrite)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, - matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\b(edit|modify|change|replace|overwrite)", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, + 1000)] private static partial Regex EditInstruction(); - [GeneratedRegex(@"\b(supported|qualified|compatible)\b", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, - matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\b(supported|qualified|compatible)\b", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, + 1000)] private static partial Regex QualificationClaim(); - [GeneratedRegex(@"\b(not|never|no|without)\b", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, - matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\b(not|never|no|without)\b", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, + 1000)] private static partial Regex NegationPattern(); - [GeneratedRegex("^[A-Za-z0-9_-]+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex("^[A-Za-z0-9_-]+$", RegexOptions.CultureInvariant, 1000)] private static partial Regex ElementId(); - [GeneratedRegex(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", RegexOptions.CultureInvariant, 1000)] private static partial Regex EmailAddress(); /// A drive path, a user-profile folder or a file: URI: never valid in a public issue form. - [GeneratedRegex(@"[A-Za-z]:\\|\\Users\\|/home/|/Users/|file://", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"[A-Za-z]:\\|\\Users\\|/home/|/Users/|file://", RegexOptions.CultureInvariant, 1000)] private static partial Regex AbsoluteLocalPath(); /// An absolute link back into this repository on main. Group rest is the path as written. - [GeneratedRegex(@"(?i:https://github\.com/CheatEngineNet/CheatEngine\.SDK)/(?:blob|tree)/main/(?[^\s)\]""'`>]*)", - RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex( + @"(?i:https://github\.com/CheatEngineNet/CheatEngine\.SDK)/(?:blob|tree)/main/(?[^\s)\]""'`>]*)", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)] private static partial Regex SelfLink(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceWorkflowTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceWorkflowTests.cs index 4e2add89..ba9f6680 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceWorkflowTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/GovernanceWorkflowTests.cs @@ -53,7 +53,8 @@ public void Governance_jobs_use_literal_runner_labels_and_timeouts() string? runner = YamlDocument.Scalar(job.Value, "runs-on"); Assert.True(runner is not null && Array.IndexOf(s_runnerLabels, runner) >= 0, $"{path} job {job.Key}: runs-on '{runner}' must be a literal windows-2025 or ubuntu-24.04 label."); - Assert.True(int.TryParse(YamlDocument.Scalar(job.Value, "timeout-minutes"), NumberStyles.None, CultureInfo.InvariantCulture, + Assert.True(int.TryParse(YamlDocument.Scalar(job.Value, "timeout-minutes"), NumberStyles.None, + CultureInfo.InvariantCulture, out int minutes) && minutes > 0, $"{path} job {job.Key} needs timeout-minutes."); } @@ -105,7 +106,8 @@ public void Governance_checkouts_never_persist_credentials() { if (YamlDocument.UsesAction(step, "actions/checkout")) { - Assert.True(string.Equals(YamlDocument.Scalar(YamlDocument.Child(step, "with"), "persist-credentials"), "false", + Assert.True(string.Equals( + YamlDocument.Scalar(YamlDocument.Child(step, "with"), "persist-credentials"), "false", StringComparison.Ordinal), $"{path} job {job.Key}: actions/checkout must set persist-credentials: false."); } @@ -126,8 +128,10 @@ public void Governance_multi_line_scripts_stop_on_the_first_error() { foreach (YamlMappingNode step in YamlDocument.Steps(job.Value)) { - string[] lines = (YamlDocument.Scalar(step, "run") ?? "").ReplaceLineEndings("\n").Trim().Split('\n'); - if (lines.Length > 1 && !string.Equals(lines[0].Trim(), "$ErrorActionPreference = 'Stop'", StringComparison.Ordinal)) + string[] lines = (YamlDocument.Scalar(step, "run") ?? "").ReplaceLineEndings("\n").Trim() + .Split('\n'); + if (lines.Length > 1 && !string.Equals(lines[0].Trim(), "$ErrorActionPreference = 'Stop'", + StringComparison.Ordinal)) { offenders.Add($"{path} job {job.Key} step '{YamlDocument.Scalar(step, "name")}'"); } @@ -165,7 +169,8 @@ public void Governance_scripts_check_the_exit_code_of_every_native_command() next++; } - if (next >= lines.Length || !lines[next].TrimStart().StartsWith("if ($LASTEXITCODE -ne 0)", StringComparison.Ordinal)) + if (next >= lines.Length || !lines[next].TrimStart() + .StartsWith("if ($LASTEXITCODE -ne 0)", StringComparison.Ordinal)) { offenders.Add($"{path} job {job.Key}: '{lines[i].Trim()}'"); } @@ -244,7 +249,8 @@ public void Codeql_analyzes_csharp_cpp_and_actions_with_literal_runner_labels() ["cpp"] = ("c-cpp", "none", "windows-2025"), ["actions"] = ("actions", "none", "ubuntu-24.04") }; - Assert.Equal(expected.Keys.Order(StringComparer.Ordinal), YamlDocument.KeysOf(YamlDocument.Child(workflow.Root, "jobs")).Order(StringComparer.Ordinal)); + Assert.Equal(expected.Keys.Order(StringComparer.Ordinal), + YamlDocument.KeysOf(YamlDocument.Child(workflow.Root, "jobs")).Order(StringComparer.Ordinal)); foreach ((string id, (string language, string buildMode, string runner)) in expected) { YamlMappingNode job = workflow.Job(id); @@ -258,7 +264,8 @@ public void Codeql_analyzes_csharp_cpp_and_actions_with_literal_runner_labels() Assert.Equal(language, YamlDocument.Scalar(init, "languages")); Assert.Equal(buildMode, YamlDocument.Scalar(init, "build-mode")); Assert.Equal("security-extended", YamlDocument.Scalar(init, "queries")); - Assert.Equal($"/language:{language}", YamlDocument.Scalar(WithOf(job, "github/codeql-action/analyze"), "category")); + Assert.Equal($"/language:{language}", + YamlDocument.Scalar(WithOf(job, "github/codeql-action/analyze"), "category")); } // Python is not analysed: the repository has no Python source left, and a language without sources fails. @@ -271,17 +278,20 @@ public void Codeql_csharp_job_builds_the_product_graph_manually_without_shared_c YamlMappingNode job = YamlDocument.Load(GovernanceWorkflows.CodeQl).Job("csharp"); IReadOnlyList steps = YamlDocument.Steps(job); - YamlMappingNode checkout = Assert.Single(steps, static step => YamlDocument.UsesAction(step, "actions/checkout")); + YamlMappingNode checkout = + Assert.Single(steps, static step => YamlDocument.UsesAction(step, "actions/checkout")); Assert.Equal("0", YamlDocument.Scalar(YamlDocument.Child(checkout, "with"), "fetch-depth")); YamlMappingNode setup = Assert.Single(steps, static step => string.Equals(YamlDocument.Uses(step), "./.github/actions/setup-dotnet", StringComparison.Ordinal)); - Assert.Equal("src/CheatEngine.SDK/CheatEngine.SDK.csproj", YamlDocument.Scalar(YamlDocument.Child(setup, "with"), "restore")); + Assert.Equal("src/CheatEngine.SDK/CheatEngine.SDK.csproj", + YamlDocument.Scalar(YamlDocument.Child(setup, "with"), "restore")); YamlMappingNode build = Assert.Single(steps, static step => YamlDocument.Scalar(step, "run") is not null); string run = YamlDocument.NormalizeWhitespace(YamlDocument.Scalar(build, "run")!); foreach (string fragment in (string[]) [ - "dotnet build src/CheatEngine.SDK/CheatEngine.SDK.csproj", "-c Release", "--no-restore", "--no-incremental", + "dotnet build src/CheatEngine.SDK/CheatEngine.SDK.csproj", "-c Release", "--no-restore", + "--no-incremental", "--disable-build-servers", "-p:UseSharedCompilation=false", "$LASTEXITCODE" ]) { @@ -300,7 +310,8 @@ public void Codeql_csharp_job_builds_the_product_graph_manually_without_shared_c } } - Assert.True(IndexOf(steps, "./.github/actions/setup-dotnet") < init && init < buildIndex && buildIndex < analyze, + Assert.True( + IndexOf(steps, "./.github/actions/setup-dotnet") < init && init < buildIndex && buildIndex < analyze, "The CodeQL C# job must restore, then init, then build, then analyze."); } @@ -371,7 +382,8 @@ public void Scorecard_steps_use_only_the_actions_the_verifier_allows() "actions/checkout", "actions/create-github-app-token", "ossf/scorecard-action", "actions/upload-artifact", "github/codeql-action/upload-sarif", "step-security/harden-runner" ]; - foreach (YamlMappingNode step in YamlDocument.Steps(YamlDocument.Load(GovernanceWorkflows.Scorecard).Job("analysis"))) + foreach (YamlMappingNode step in YamlDocument.Steps(YamlDocument.Load(GovernanceWorkflows.Scorecard) + .Job("analysis"))) { string uses = YamlDocument.Uses(step) ?? ""; string action = uses.Split('@')[0]; @@ -426,7 +438,8 @@ public void Zizmor_online_pins_the_tool_version_and_enables_online_audits() // Fork pull requests cannot upload SARIF, and drafts wait. string condition = YamlDocument.NormalizeWhitespace(YamlDocument.Scalar(job, "if") ?? ""); - Assert.Contains("github.event.pull_request.head.repo.full_name == github.repository", condition, StringComparison.Ordinal); + Assert.Contains("github.event.pull_request.head.repo.full_name == github.repository", condition, + StringComparison.Ordinal); Assert.Contains("!github.event.pull_request.draft", condition, StringComparison.Ordinal); Assert.Equal(["push", "pull_request", "schedule", "workflow_dispatch"], workflow.Triggers); } @@ -443,7 +456,8 @@ public void Online_and_gate_zizmor_runs_pin_the_same_version() { if (YamlDocument.UsesAction(step, "zizmorcore/zizmor-action")) { - Assert.True(string.Equals(YamlDocument.Scalar(YamlDocument.Child(step, "with"), "version"), ZizmorVersion, + Assert.True(string.Equals(YamlDocument.Scalar(YamlDocument.Child(step, "with"), "version"), + ZizmorVersion, StringComparison.Ordinal), $"{path} job {job.Key} must pin zizmor {ZizmorVersion} like {GovernanceWorkflows.ZizmorOnline}."); } @@ -486,7 +500,8 @@ public void Only_the_dependency_submit_job_holds_contents_write() } Assert.Equal([GovernanceWorkflows.DependencySubmission + "#submit"], writers); - Assert.Null(YamlDocument.Permissions(YamlDocument.Load(GovernanceWorkflows.DependencySubmission).Job("detect"))); + Assert.Null( + YamlDocument.Permissions(YamlDocument.Load(GovernanceWorkflows.DependencySubmission).Job("detect"))); } [Fact] @@ -496,7 +511,8 @@ public void Dependency_submit_job_runs_no_third_party_code() IReadOnlyList steps = YamlDocument.Steps(workflow.Job("submit")); Assert.Equal(2, steps.Count); - Assert.True(YamlDocument.UsesAction(steps[0], "actions/download-artifact"), "submit starts by downloading the snapshot."); + Assert.True(YamlDocument.UsesAction(steps[0], "actions/download-artifact"), + "submit starts by downloading the snapshot."); Assert.Equal("dependency-snapshot", YamlDocument.Scalar(YamlDocument.Child(steps[0], "with"), "name")); Assert.Null(YamlDocument.Uses(steps[1])); string run = YamlDocument.Scalar(steps[1], "run") ?? ""; @@ -512,7 +528,8 @@ public void Dependency_submit_job_runs_no_third_party_code() } // The official action fetches the latest Component Detection at run time next to the write token. - Assert.DoesNotContain("component-detection-dependency-submission-action", workflow.Text, StringComparison.Ordinal); + Assert.DoesNotContain("component-detection-dependency-submission-action", workflow.Text, + StringComparison.Ordinal); } [Fact] @@ -525,8 +542,12 @@ public void Dependency_detection_uses_a_pinned_hash_verified_component_detection "Run Component Detection and build the snapshot", StringComparison.Ordinal)); string script = YamlDocument.Scalar(step, "run") ?? ""; - Assert.Matches(new Regex(@"^\s*\$detectorVersion = '\d+\.\d+\.\d+'\r?$", RegexOptions.Multiline, TimeSpan.FromSeconds(1)), script); - Assert.Matches(new Regex(@"^\s*\$detectorSha256 = '[0-9a-f]{64}'\r?$", RegexOptions.Multiline, TimeSpan.FromSeconds(1)), script); + Assert.Matches( + new Regex(@"^\s*\$detectorVersion = '\d+\.\d+\.\d+'\r?$", RegexOptions.Multiline, TimeSpan.FromSeconds(1)), + script); + Assert.Matches( + new Regex(@"^\s*\$detectorSha256 = '[0-9a-f]{64}'\r?$", RegexOptions.Multiline, TimeSpan.FromSeconds(1)), + script); Assert.Contains("releases/download/v$detectorVersion/$detectorAsset", script, StringComparison.Ordinal); Assert.Contains("Get-FileHash -LiteralPath $detector -Algorithm SHA256", script, StringComparison.Ordinal); Assert.Contains("--locked-mode", script, StringComparison.Ordinal); @@ -547,31 +568,13 @@ public async Task Dependency_submit_step_submits_only_a_snapshot_of_this_run(str const string Ref = "refs/heads/main"; using TemporaryDirectory directory = new(); Directory.CreateDirectory(directory.File("snapshot")); - Dictionary snapshot = new(StringComparer.Ordinal) - { - ["version"] = 0, - ["sha"] = Is(variant, "other_commit") ? new string('f', 40) : Sha, - ["ref"] = Is(variant, "other_ref") ? "refs/heads/feature" : Ref, - ["job"] = new Dictionary(StringComparer.Ordinal) - { - ["correlator"] = Is(variant, "other_correlator") ? "other" : "sdk-nuget", - ["id"] = "1" - }, - ["detector"] = new Dictionary(StringComparer.Ordinal) { ["name"] = "d", ["version"] = "1", ["url"] = "u" }, - ["scanned"] = "2026-09-23T00:00:00Z", - ["manifests"] = Is(variant, "no_manifest") - ? new Dictionary(StringComparer.Ordinal) - : new Dictionary(StringComparer.Ordinal) { ["src/A.csproj"] = new Dictionary(StringComparer.Ordinal) } - }; - if (Is(variant, "extra_property")) - { - snapshot["extra"] = 1; - } + Dictionary snapshot = BuildDependencySnapshot(variant, Sha, Ref); await File.WriteAllTextAsync(directory.File("snapshot/snapshot.json"), JsonSerializer.Serialize(snapshot), TestContext.Current.CancellationToken); - YamlMappingNode submit = YamlDocument.Steps(YamlDocument.Load(GovernanceWorkflows.DependencySubmission).Job("submit"))[1]; + YamlMappingNode submit = + YamlDocument.Steps(YamlDocument.Load(GovernanceWorkflows.DependencySubmission).Job("submit"))[1]; string marker = directory.File("gh-called.txt"); string script = $"Set-Location -LiteralPath {PwshScript.Quote(directory.Path)}" + Environment.NewLine + $"function gh {{ $args -join ' ' | Set-Content -LiteralPath {PwshScript.Quote(marker)}; $global:LASTEXITCODE = 0 }}" + @@ -605,10 +608,46 @@ private static bool Is(string value, string expected) return string.Equals(value, expected, StringComparison.Ordinal); } + private static Dictionary BuildDependencySnapshot(string variant, string sha, string @ref) + { + Dictionary snapshot = new(StringComparer.Ordinal) + { + ["version"] = 0, + ["sha"] = Is(variant, "other_commit") ? new string('f', 40) : sha, + ["ref"] = Is(variant, "other_ref") ? "refs/heads/feature" : @ref, + ["job"] = new Dictionary(StringComparer.Ordinal) + { + ["correlator"] = Is(variant, "other_correlator") ? "other" : "sdk-nuget", + ["id"] = "1" + }, + ["detector"] = + new Dictionary(StringComparer.Ordinal) + { + ["name"] = "d", + ["version"] = "1", + ["url"] = "u" + }, + ["scanned"] = "2026-09-23T00:00:00Z", + ["manifests"] = Is(variant, "no_manifest") + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(StringComparer.Ordinal) + { + ["src/A.csproj"] = new Dictionary(StringComparer.Ordinal) + } + }; + if (Is(variant, "extra_property")) + { + snapshot["extra"] = 1; + } + + return snapshot; + } + private static YamlNode? WithOf(YamlMappingNode job, string action) { YamlMappingNode step = Assert.Single(YamlDocument.Steps(job), step => YamlDocument.UsesAction(step, action) - || string.Equals(YamlDocument.Uses(step), action, StringComparison.Ordinal)); + || string.Equals(YamlDocument.Uses(step), + action, StringComparison.Ordinal)); return YamlDocument.Child(step, "with"); } @@ -626,17 +665,18 @@ private static int IndexOf(IReadOnlyList steps, string action) return -1; } - [GeneratedRegex(@"^\s*(-\s+)?uses:\s+(?\S+)(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, - matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s*(-\s+)?uses:\s+(?\S+)(?.*)$", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + 1000)] private static partial Regex UsesLine(); - [GeneratedRegex(@"^[A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+@[0-9a-f]{40}$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^[A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+@[0-9a-f]{40}$", RegexOptions.CultureInvariant, 1000)] private static partial Regex PinnedReference(); - [GeneratedRegex(@"^\s+# v\d+\.\d+\.\d+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s+# v\d+\.\d+\.\d+$", RegexOptions.CultureInvariant, 1000)] private static partial Regex VersionComment(); /// A line that starts a native command or a repository script (same pattern as WorkflowContractTests). - [GeneratedRegex(@"^\s*(?:dotnet|xmake|git|gh|tar)\s|^\s*\./|^\s*&\s", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s*(?:dotnet|xmake|git|gh|tar)\s|^\s*\./|^\s*&\s", RegexOptions.CultureInvariant, 1000)] private static partial Regex NativeInvocation(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshResult.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshResult.cs index 036ae325..1f42a36c 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshResult.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshResult.cs @@ -4,5 +4,6 @@ namespace CheatEngine.SDK.Repository.Tests.Governance; internal sealed record PwshResult(int ExitCode, string StandardOutput, string StandardError) { /// Both streams, for assertion messages. - public string Transcript => $"exit code {ExitCode}{Environment.NewLine}--- stdout ---{Environment.NewLine}{StandardOutput}{Environment.NewLine}--- stderr ---{Environment.NewLine}{StandardError}"; + public string Transcript => + $"exit code {ExitCode}{Environment.NewLine}--- stdout ---{Environment.NewLine}{StandardOutput}{Environment.NewLine}--- stderr ---{Environment.NewLine}{StandardError}"; } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshScript.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshScript.cs index e6974a49..bdd9f3f3 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshScript.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/PwshScript.cs @@ -23,7 +23,8 @@ internal static class PwshScript [ "GITHUB_STEP_SUMMARY", "GITHUB_OUTPUT", "GITHUB_ENV", "GITHUB_PATH", "GITHUB_STATE", "GITHUB_ACTIONS", "CI", "PR_TITLE", "PR_BODY", "PR_AUTHOR", "BASE_SHA", "HEAD_SHA", "GH_TOKEN", "GITHUB_TOKEN", "GH_REPO", - "NEEDS", "RUN_URL", "DRIFT", "BROKEN_LINKS", "REPOSITORY", "SNAPSHOT_SHA", "SNAPSHOT_REF", "SNAPSHOT_CORRELATOR", + "NEEDS", "RUN_URL", "DRIFT", "BROKEN_LINKS", "REPOSITORY", "SNAPSHOT_SHA", "SNAPSHOT_REF", + "SNAPSHOT_CORRELATOR", "SNAPSHOT_JOB_ID", "SNAPSHOT_JOB_URL", "CESDK_PACKAGED_UMBRELLA_NUPKG" ]; @@ -95,9 +96,10 @@ public static async Task RunFileAsync(string scriptPath, IReadOnlyLi } catch (OperationCanceledException) { - process.Kill(entireProcessTree: true); + process.Kill(true); TestContext.Current.CancellationToken.ThrowIfCancellationRequested(); - Assert.Fail($"pwsh did not finish '{RepositoryRoot.ToRelative(fullPath)}' within {s_timeout.TotalSeconds} s."); + Assert.Fail( + $"pwsh did not finish '{RepositoryRoot.ToRelative(fullPath)}' within {s_timeout.TotalSeconds} s."); } return new PwshResult(process.ExitCode, await standardOutput, await standardError); diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/TemporaryDirectory.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/TemporaryDirectory.cs index be241ad8..74ce04eb 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/TemporaryDirectory.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/TemporaryDirectory.cs @@ -15,17 +15,11 @@ public string Path get; } - /// The absolute path of a file inside the folder. - public string File(string name) - { - return System.IO.Path.Combine(Path, name); - } - public void Dispose() { try { - Directory.Delete(Path, recursive: true); + Directory.Delete(Path, true); } catch (IOException) { @@ -36,4 +30,10 @@ public void Dispose() // Same as above. } } + + /// The absolute path of a file inside the folder. + public string File(string name) + { + return System.IO.Path.Combine(Path, name); + } } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Governance/YamlDocument.cs b/tests/CheatEngine.SDK.Repository.Tests/Governance/YamlDocument.cs index e35dcc97..be8ba19b 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Governance/YamlDocument.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Governance/YamlDocument.cs @@ -5,7 +5,8 @@ namespace CheatEngine.SDK.Repository.Tests.Governance; /// /// A committed YAML file (workflow, Dependabot configuration, issue form) read through YamlDotNet's representation /// model. Every scalar stays the string written in the file: the workflow key on stays "on" (no YAML 1.1 -/// boolean resolution) and 'true' and true both read as "true", which is how GitHub compares them. +/// boolean resolution) and 'true' and true both read as "true", which is how GitHub compares +/// them. /// internal sealed class YamlDocument { @@ -46,7 +47,8 @@ public IReadOnlyList> Jobs { if (entry.Value is YamlMappingNode job) { - jobs.Add(new KeyValuePair(((YamlScalarNode) entry.Key).Value ?? "", job)); + jobs.Add(new KeyValuePair(((YamlScalarNode) entry.Key).Value ?? "", + job)); } } } diff --git a/tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs b/tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs index b279cec4..fab5fc93 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs @@ -11,14 +11,15 @@ namespace CheatEngine.SDK.Repository.Tests.HostContract; /// public sealed class ConcurrencyContractDocumentationTests { + // This test's own source quotes the banned phrases verbatim (as the pattern it looks for and in this remark), so + // it is excluded from its own scan; every other file under libs/ and tests/ is still checked. + private const string SelfPath = + "tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs"; + private static readonly Regex UnqualifiedCardinalityClaim = new( @"\(one per plugin\)|one load context hosts one plugin|hosted by this assembly load context", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1)); - // This test's own source quotes the banned phrases verbatim (as the pattern it looks for and in this remark), so - // it is excluded from its own scan; every other file under libs/ and tests/ is still checked. - private const string SelfPath = "tests/CheatEngine.SDK.Repository.Tests/HostContract/ConcurrencyContractDocumentationTests.cs"; - [Fact] public void Sources_and_readmes_never_claim_one_runtime_or_load_context_per_plugin() { @@ -26,8 +27,8 @@ public void Sources_and_readmes_never_claim_one_runtime_or_load_context_per_plug foreach (string relative in RepositoryRoot.EnumerateSourceFiles("*.cs") .Concat(RepositoryRoot.EnumerateSourceFiles("*.md")) .Where(static path => (path.StartsWith("libs/", StringComparison.Ordinal) - || path.StartsWith("tests/", StringComparison.Ordinal)) - && !string.Equals(path, SelfPath, StringComparison.Ordinal)) + || path.StartsWith("tests/", StringComparison.Ordinal)) + && !string.Equals(path, SelfPath, StringComparison.Ordinal)) .Distinct(StringComparer.Ordinal) .Order(StringComparer.Ordinal)) { @@ -39,13 +40,15 @@ public void Sources_and_readmes_never_claim_one_runtime_or_load_context_per_plug } Assert.True(offenders.Count == 0, - "Unqualified plugin-cardinality claim(s):" + Environment.NewLine + string.Join(Environment.NewLine, offenders)); + "Unqualified plugin-cardinality claim(s):" + Environment.NewLine + + string.Join(Environment.NewLine, offenders)); } [Fact] public void Hosting_readme_publishes_the_ADR_07_concurrency_contract() { - string readme = File.ReadAllText(Path.Combine(RepositoryRoot.Path, "libs", "CheatEngine.SDK.Hosting", "README.md")); + string readme = + File.ReadAllText(Path.Combine(RepositoryRoot.Path, "libs", "CheatEngine.SDK.Hosting", "README.md")); Assert.Contains("Threading and Lua concurrency contract (ADR-07)", readme, StringComparison.Ordinal); int sectionStart = readme.IndexOf("Threading and Lua concurrency contract (ADR-07)", StringComparison.Ordinal); string section = readme[sectionStart..]; diff --git a/tests/CheatEngine.SDK.Repository.Tests/HostContract/NativeHostEmulatorScriptTests.cs b/tests/CheatEngine.SDK.Repository.Tests/HostContract/NativeHostEmulatorScriptTests.cs index 6e1b6f24..b12e6ca5 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/HostContract/NativeHostEmulatorScriptTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/HostContract/NativeHostEmulatorScriptTests.cs @@ -52,7 +52,8 @@ public void Emulator_scripts_never_reference_a_cheat_engine_installation() continue; } - string text = File.ReadAllText(Path.Combine(RepositoryRoot.Path, relativePath.Replace('/', Path.DirectorySeparatorChar))); + string text = File.ReadAllText(Path.Combine(RepositoryRoot.Path, + relativePath.Replace('/', Path.DirectorySeparatorChar))); if (CheatEngineInstallationReference().IsMatch(text)) { offendingFiles.Add(relativePath); @@ -77,7 +78,8 @@ public void Build_script_writes_only_under_its_output_directory() } string codePortion = CommentStart().Split(line)[0]; - Assert.False(DriveLetterPath().IsMatch(codePortion), $"Hardcoded drive-letter path outside a comment: '{line.Trim()}'"); + Assert.False(DriveLetterPath().IsMatch(codePortion), + $"Hardcoded drive-letter path outside a comment: '{line.Trim()}'"); } } @@ -86,22 +88,25 @@ private static string RepositoryFile(string relativePath) return Path.Combine(RepositoryRoot.Path, relativePath.Replace('/', Path.DirectorySeparatorChar)); } - [GeneratedRegex(@"&\s*\$compiler\.Source\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"&\s*\$compiler\.Source\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex NativeCommandInvocation(); - [GeneratedRegex(@"Program Files\\Cheat Engine|cheatengine-x86_64", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"Program Files\\Cheat Engine|cheatengine-x86_64", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, 1000)] private static partial Regex CheatEngineInstallationReference(); - [GeneratedRegex(@"^\s*(Copy-Item|Set-Content).*$", RegexOptions.CultureInvariant | RegexOptions.Multiline | RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s*(Copy-Item|Set-Content).*$", + RegexOptions.CultureInvariant | RegexOptions.Multiline | RegexOptions.ExplicitCapture, 1000)] private static partial Regex WriteCommand(); - [GeneratedRegex(@"resolvedOutputDirectory|nethostDllDestination|runtimeConfigDestination|manifestPath|\$exe\b|\$objectFile\b", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex( + @"resolvedOutputDirectory|nethostDllDestination|runtimeConfigDestination|manifestPath|\$exe\b|\$objectFile\b", + RegexOptions.CultureInvariant, 1000)] private static partial Regex OutputDirectoryDerivedVariable(); - [GeneratedRegex(@"[A-Za-z]:\\", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"[A-Za-z]:\\", RegexOptions.CultureInvariant, 1000)] private static partial Regex DriveLetterPath(); - [GeneratedRegex(@"(? 0, "No project sets RuntimeIdentifier: the Native AOT probes are expected to."); + Assert.True(runtimeSpecificProjects > 0, + "No project sets RuntimeIdentifier: the Native AOT probes are expected to."); } [Fact] diff --git a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaApiUseScanner.cs b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaApiUseScanner.cs index 0ca857fd..130038b4 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaApiUseScanner.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaApiUseScanner.cs @@ -38,11 +38,11 @@ internal static IReadOnlyList Scan(string source, IReadOnlySet(?:global::)?(?:CheatEngine\.SDK\.Lua\.Interop\.Api\.)?LuaApi\s*\.\s*)?\b(?(?:lua|luaL|luaopen)_\w+)\b", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + RegexOptions.CultureInvariant, 1000)] private static partial Regex MemberReference(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaBridgeDocuments.cs b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaBridgeDocuments.cs index 0b13608d..311fb696 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaBridgeDocuments.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaBridgeDocuments.cs @@ -14,7 +14,10 @@ namespace CheatEngine.SDK.Repository.Tests.LuaBridge; internal static class LuaBridgeDocuments { internal const string CataloguePath = "libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json"; - internal const string MatrixPath = "tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json"; + + internal const string MatrixPath = + "tests/CheatEngine.SDK.Repository.Tests/LuaBridge/TestData/lua-interop-primitives.json"; + internal const string BridgeSourcePath = "native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c"; internal const string XmakePath = "native/cheatengine-sdk-lua-bridge/xmake.lua"; internal const string LuaFixturePath = "native/cheat-engine/lua53-64.dll"; diff --git a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaInteropPrimitiveMatrixDocumentTests.cs b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaInteropPrimitiveMatrixDocumentTests.cs index 6e958eec..707bf018 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaInteropPrimitiveMatrixDocumentTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/LuaBridge/LuaInteropPrimitiveMatrixDocumentTests.cs @@ -28,7 +28,8 @@ public sealed class LuaInteropPrimitiveMatrixDocumentTests [Fact] public void Primitive_matrix_has_its_schema_identity() { - Assert.Equal("cheatengine-lua-interop-primitives/v0", LuaBridgeDocuments.Matrix.GetProperty("schema").GetString()); + Assert.Equal("cheatengine-lua-interop-primitives/v0", + LuaBridgeDocuments.Matrix.GetProperty("schema").GetString()); } [Fact] @@ -44,7 +45,8 @@ public void Primitive_matrix_is_canonical_and_its_lists_are_sorted_and_unique() string[] exceptions = [ .. matrix.GetProperty("productionExceptions").EnumerateArray() - .Select(static item => item.GetProperty("member").GetString() + "|" + item.GetProperty("file").GetString()) + .Select(static item => + item.GetProperty("member").GetString() + "|" + item.GetProperty("file").GetString()) ]; Assert.Equal(exceptions.Order(StringComparer.Ordinal), exceptions, StringComparer.Ordinal); Assert.Equal(exceptions.Length, exceptions.Distinct(StringComparer.Ordinal).Count()); @@ -56,7 +58,8 @@ public void Rows_are_consistent_with_their_decision_bridge_operation_and_manual_ HashSet operations = new(LuaBridgeDocuments.Operations.Select(static operation => operation.GetProperty("id").GetString()!), StringComparer.Ordinal); Dictionary divergences = LuaBridgeDocuments.Matrix.GetProperty("manualDivergences") - .EnumerateArray().ToDictionary(static item => item.GetProperty("member").GetString()!, StringComparer.Ordinal); + .EnumerateArray().ToDictionary(static item => item.GetProperty("member").GetString()!, + StringComparer.Ordinal); List problems = []; foreach (JsonElement row in LuaBridgeDocuments.Rows) { @@ -65,7 +68,8 @@ public void Rows_are_consistent_with_their_decision_bridge_operation_and_manual_ } Assert.True(problems.Count == 0, string.Join(Environment.NewLine, problems)); - HashSet members = new(LuaBridgeDocuments.Rows.Select(static row => row.GetProperty("member").GetString()!), + HashSet members = new( + LuaBridgeDocuments.Rows.Select(static row => row.GetProperty("member").GetString()!), StringComparer.Ordinal); Assert.All(divergences.Keys, member => Assert.Contains(member, members)); } @@ -77,7 +81,8 @@ public void Production_raw_LuaApi_uses_are_classified_or_explicitly_excepted() static row => row.GetProperty("member").GetString()!, static row => row.GetProperty("decision").GetString()!, StringComparer.Ordinal); HashSet excepted = new(LuaBridgeDocuments.Matrix.GetProperty("productionExceptions").EnumerateArray() - .Select(static item => item.GetProperty("member").GetString() + "|" + item.GetProperty("file").GetString()), + .Select(static item => + item.GetProperty("member").GetString() + "|" + item.GetProperty("file").GetString()), StringComparer.Ordinal); HashSet members = new(decisions.Keys, StringComparer.Ordinal); HashSet used = new(StringComparer.Ordinal); @@ -91,7 +96,8 @@ public void Production_raw_LuaApi_uses_are_classified_or_explicitly_excepted() used.Add(key); if (decisions[use.Member] is not ("DirectAllowed" or "Lifecycle") && !excepted.Contains(key)) { - problems.Add($"{file}:{use.Line}: {use.Member} is {decisions[use.Member]}; route it through the bridge or add a production exception."); + problems.Add( + $"{file}:{use.Line}: {use.Member} is {decisions[use.Member]}; route it through the bridge or add a production exception."); } } } @@ -111,16 +117,16 @@ public void Scanner_sees_qualified_and_static_uses_and_ignores_comments_and_lite { HashSet members = new(["lua_tolstring", "lua_settop", "lua_gettop"], StringComparer.Ordinal); const string Source = """" - using static CheatEngine.SDK.Lua.Interop.Api.LuaApi; - // lua_gettop(L) in a comment - /* LuaApi.lua_gettop */ - class C - { - string s = "lua_gettop(L)"; - string v = @"LuaApi.lua_gettop ""quoted"""; - void M(void* L) { lua_settop(L, 0); _ = LuaApi.lua_tolstring(L, -1, null); other.lua_gettop(); } - } - """"; + using static CheatEngine.SDK.Lua.Interop.Api.LuaApi; + // lua_gettop(L) in a comment + /* LuaApi.lua_gettop */ + class C + { + string s = "lua_gettop(L)"; + string v = @"LuaApi.lua_gettop ""quoted"""; + void M(void* L) { lua_settop(L, 0); _ = LuaApi.lua_tolstring(L, -1, null); other.lua_gettop(); } + } + """"; IReadOnlyList uses = LuaApiUseScanner.Scan(Source, members); @@ -139,7 +145,8 @@ public void Checkstack_proof_record_matches_the_bundled_lua_and_the_lf_pinned_br proof.GetProperty("bridgeSourceSha256").GetString()); Assert.Equal(RepositoryDocument.RawSha256(LuaBridgeDocuments.XmakePath), proof.GetProperty("xmakeSha256").GetString()); - Assert.DoesNotContain((byte) '\r', File.ReadAllBytes(RepositoryDocument.Absolute(LuaBridgeDocuments.BridgeSourcePath))); + Assert.DoesNotContain((byte) '\r', + File.ReadAllBytes(RepositoryDocument.Absolute(LuaBridgeDocuments.BridgeSourcePath))); string[] test = proof.GetProperty("checkstackEvidence").GetString()!.Split('.'); string file = Assert.Single(RepositoryRoot.EnumerateSourceFiles(test[0] + ".cs"), @@ -152,9 +159,10 @@ public void Bundled_lua_fixture_hash_equals_the_catalogue_and_primitive_matrix_h { string fixture = RepositoryDocument.RawSha256(LuaBridgeDocuments.LuaFixturePath); - Assert.Equal(fixture, LuaBridgeDocuments.Matrix.GetProperty("bridgeProof").GetProperty("luaFixtureSha256").GetString()); + Assert.Equal(fixture, + LuaBridgeDocuments.Matrix.GetProperty("bridgeProof").GetProperty("luaFixtureSha256").GetString()); Assert.Equal(fixture, LuaBridgeDocuments.Catalogue.GetProperty("host").GetProperty("lua") - .GetProperty("fixtureSha256").GetString(), ignoreCase: true); + .GetProperty("fixtureSha256").GetString(), true); } private static void CheckRoute(JsonElement row, HashSet operations, List problems) @@ -164,7 +172,7 @@ private static void CheckRoute(JsonElement row, HashSet operations, List string raises = row.GetProperty("raises").GetString()!; string? bridge = LuaBridgeDocuments.OptionalString(row, "bridgeOperation"); string? native = LuaBridgeDocuments.OptionalString(row, "nativeSymbol"); - if (raises is "Memory" or "Any" or "Always" && (decision is "DirectAllowed" or "Lifecycle")) + if (raises is "Memory" or "Any" or "Always" && decision is "DirectAllowed" or "Lifecycle") { problems.Add($"{member}: a raising member cannot be {decision}."); } @@ -177,7 +185,8 @@ private static void CheckRoute(JsonElement row, HashSet operations, List if (string.Equals(decision, "BridgeRequired", StringComparison.Ordinal) && bridge is null && !string.Equals(member, "lua_error", StringComparison.Ordinal)) { - problems.Add($"{member}: BridgeRequired names no bridge operation (only lua_error, raised by the bridge's C code, may)."); + problems.Add( + $"{member}: BridgeRequired names no bridge operation (only lua_error, raised by the bridge's C code, may)."); } if (native is not null && !string.Equals(native, member, StringComparison.Ordinal)) @@ -186,7 +195,8 @@ private static void CheckRoute(JsonElement row, HashSet operations, List } } - private static void CheckProvenance(JsonElement row, Dictionary divergences, List problems) + private static void CheckProvenance(JsonElement row, Dictionary divergences, + List problems) { string member = row.GetProperty("member").GetString()!; JsonElement provenance = row.GetProperty("provenance"); @@ -218,7 +228,8 @@ private static void CheckProvenance(JsonElement row, Dictionary operation.GetProperty("id").GetString()!); AssertUnique(operations, static operation => operation.GetProperty("nativeEnum").GetString()!); - AssertUnique(operations, static operation => operation.GetProperty("managed").GetProperty("constant").GetString()!); - AssertUnique(operations, static operation => operation.GetProperty("managed").GetProperty("wrapper").GetString()!); + AssertUnique(operations, + static operation => operation.GetProperty("managed").GetProperty("constant").GetString()!); + AssertUnique(operations, + static operation => operation.GetProperty("managed").GetProperty("wrapper").GetString()!); Assert.Equal(BitmapWidth, contract.GetProperty("operationBitmapWidth").GetInt32()); Assert.Equal(string.Create(CultureInfo.InvariantCulture, $"0x{bitmap:X16}"), contract.GetProperty("operationBitmap").GetString()); @@ -79,11 +82,13 @@ public void Every_operation_has_its_native_enum_value_and_switch_case_in_the_lf_ { string name = Regex.Escape(operation.GetProperty("nativeEnum").GetString()!); int opcode = operation.GetProperty("opcode").GetInt32(); - Assert.True(Regex.IsMatch(values, $@"\b{name}\s*=\s*{opcode}\b", RegexOptions.CultureInvariant, RegexTimeout), + Assert.True( + Regex.IsMatch(values, $@"\b{name}\s*=\s*{opcode}\b", RegexOptions.CultureInvariant, RegexTimeout), $"The native enum does not define {name} = {opcode}."); Assert.True(Regex.IsMatch(source, $@"\bcase\s+{name}\s*:", RegexOptions.CultureInvariant, RegexTimeout), $"The native bridge has no case for {name}."); - Assert.True(Regex.IsMatch(mask.Groups["mask"].Value, $@"\b{name}\b", RegexOptions.CultureInvariant, RegexTimeout), + Assert.True( + Regex.IsMatch(mask.Groups["mask"].Value, $@"\b{name}\b", RegexOptions.CultureInvariant, RegexTimeout), $"The native operation mask does not name {name}."); } } @@ -120,7 +125,8 @@ public void Direct_api_policy_routes_are_exclusive_explicit_and_never_direct_for if (policy.TryGetProperty("conditionalDirectUse", out JsonElement conditional) && (direct || !bridge || !conditional.GetProperty("allowed").GetBoolean())) { - problems.Add($"{symbol}: a conditional direct use keeps the bridge as its default route and opts in explicitly."); + problems.Add( + $"{symbol}: a conditional direct use keeps the bridge as its default route and opts in explicitly."); } } @@ -135,7 +141,8 @@ public void Every_raising_operation_names_a_failure_test() foreach (JsonElement operation in LuaBridgeDocuments.Operations) { string id = operation.GetProperty("id").GetString()!; - bool raises = !string.Equals(operation.GetProperty("raises").GetString(), "never", StringComparison.Ordinal); + bool raises = !string.Equals(operation.GetProperty("raises").GetString(), "never", + StringComparison.Ordinal); if (raises && (!operation.TryGetProperty("failureEvidence", out JsonElement evidence) || evidence.GetArrayLength() == 0)) { @@ -170,7 +177,8 @@ public void Managed_failure_evidence_resolves_to_a_Q13_traited_method() public void Probe_marker_evidence_is_emitted_by_the_failure_probe_source() { string probe = RepositoryDocument.ReadNormalizedText(LuaBridgeDocuments.FailureProbePath); - string[] markers = [.. Evidence(ProbeMarkerKind).Select(static item => item.GetProperty("marker").GetString()!)]; + string[] markers = + [.. Evidence(ProbeMarkerKind).Select(static item => item.GetProperty("marker").GetString()!)]; Assert.NotEmpty(markers); Assert.Equal(markers.Length, markers.Distinct(StringComparer.Ordinal).Count()); @@ -199,8 +207,10 @@ public void Retired_live_ids_are_absent() } JsonElement pusher = Assert.Single(LuaBridgeDocuments.Operations, - static operation => string.Equals(operation.GetProperty("id").GetString(), "PushHostObject", StringComparison.Ordinal)); - Assert.Contains("Q23", pusher.GetProperty("provenance")[0].GetProperty("verification").GetString(), StringComparison.Ordinal); + static operation => string.Equals(operation.GetProperty("id").GetString(), "PushHostObject", + StringComparison.Ordinal)); + Assert.Contains("Q23", pusher.GetProperty("provenance")[0].GetProperty("verification").GetString(), + StringComparison.Ordinal); Assert.Contains("Q23", RepositoryDocument.ReadNormalizedText("libs/CheatEngine.SDK.Lua.Interop/README.md"), StringComparison.Ordinal); } @@ -226,20 +236,24 @@ private static IEnumerable Evidence(string kind) private static void AssertUnique(JsonElement[] items, Func key) { - string[] duplicates = [.. items.GroupBy(key, StringComparer.Ordinal).Where(static group => group.Count() > 1) - .Select(static group => group.Key)]; + string[] duplicates = + [ + .. items.GroupBy(key, StringComparer.Ordinal).Where(static group => group.Count() > 1) + .Select(static group => group.Key) + ]; Assert.True(duplicates.Length == 0, "Duplicated: " + string.Join(", ", duplicates)); } - [GeneratedRegex(@"enum\s*\{\s*(?OP_PUSH_BYTES\s*=\s*0,[\s\S]*?CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT\s*=\s*\d+)\s*\};", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex( + @"enum\s*\{\s*(?OP_PUSH_BYTES\s*=\s*0,[\s\S]*?CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT\s*=\s*\d+)\s*\};", + RegexOptions.CultureInvariant, 1000)] private static partial Regex NativeOperationEnum(); [GeneratedRegex(@"CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT\s*=\s*(?\d+)", RegexOptions.CultureInvariant, - matchTimeoutMilliseconds: 1000)] + 1000)] private static partial Regex OperationCount(); [GeneratedRegex(@"#define\s+CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_MASK\s*\\(?[\s\S]*?)\n\s*\n", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + RegexOptions.CultureInvariant, 1000)] private static partial Regex OperationMask(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Ownership/OwnershipPolicyTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Ownership/OwnershipPolicyTests.cs index 83587689..0667ce1d 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Ownership/OwnershipPolicyTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Ownership/OwnershipPolicyTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.RegularExpressions; using CheatEngine.SDK.Repository.Tests.Infrastructure; @@ -51,7 +52,8 @@ public void No_shipping_library_binds_delete_all_registered_symbols() } Assert.True(offenders.Count == 0, - DeleteAllRegisteredSymbols + " removes every plugin's and script's symbols; bind per-plugin cleanup instead: " + + DeleteAllRegisteredSymbols + + " removes every plugin's and script's symbols; bind per-plugin cleanup instead: " + string.Join(", ", offenders)); } @@ -98,7 +100,7 @@ private static List FindFinalizers(string file, string[] lines) if (FinalizerDeclaration.IsMatch(line)) { - hits.Add(file + ":" + (index + 1).ToString(System.Globalization.CultureInfo.InvariantCulture)); + hits.Add(file + ":" + (index + 1).ToString(CultureInfo.InvariantCulture)); } } diff --git a/tests/CheatEngine.SDK.Repository.Tests/PublicApi/EnumContractTests.cs b/tests/CheatEngine.SDK.Repository.Tests/PublicApi/EnumContractTests.cs index 8b15f7a4..6d15c9d0 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/PublicApi/EnumContractTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/PublicApi/EnumContractTests.cs @@ -80,14 +80,20 @@ public sealed class EnumContractTests /// declared through CompatibilitySuppressions.xml when the enum shipped, and a PublicAPI change). The list only /// shrinks: fails once an entry is fixed. /// - private static readonly Dictionary s_pendingZeroValueFixes = new(StringComparer.Ordinal) - { - }; + private static readonly Dictionary s_pendingZeroValueFixes = new(StringComparer.Ordinal); /// Zero-member names that read as "it worked" (or, for a failure-kind enum, "no failure"). private static readonly HashSet s_successLikeNames = new(StringComparer.Ordinal) { - "Complete", "Completed", "Done", "None", "Ok", "Released", "Succeeded", "Success", "Successful" + "Complete", + "Completed", + "Done", + "None", + "Ok", + "Released", + "Succeeded", + "Success", + "Successful" }; /// @@ -99,15 +105,6 @@ public sealed class EnumContractTests "Unknown", "Unspecified", "Uninitialized", "NotAttempted" }; - private enum EnumCategory - { - Unknown = 0, - StatusOrOutcome, - Policy, - ReasonOrEvidence, - AbiDecision - } - [Fact] public void Enums_mirroring_cheat_engine_constants_or_client_signatures_keep_their_1_0_0_members() { @@ -168,7 +165,8 @@ public void Status_and_outcome_enums_added_after_1_0_0_do_not_default_to_success } string? zero = ZeroMember(declared[enumType]); - Assert.True(zero is not null, $"{enumType} has no member with value 0, so default({SimpleName(enumType)}) has no name."); + Assert.True(zero is not null, + $"{enumType} has no member with value 0, so default({SimpleName(enumType)}) has no name."); Assert.False(s_successLikeNames.Contains(zero), $"{enumType}.{zero} = 0 reads as success."); Assert.True(s_neutralZeroNames.Contains(zero), $"{enumType}.{zero} = 0: a status/outcome enum starts with Unknown = 0 (tolerated: {string.Join(", ", s_neutralZeroNames)})."); @@ -181,10 +179,14 @@ public void Pending_zero_value_fixes_are_still_needed() Dictionary> declared = EnumMembers(static l => l.Declared); foreach ((string enumType, string owner) in s_pendingZeroValueFixes) { - Assert.True(s_addedEnums.TryGetValue(enumType, out EnumCategory category) && category == EnumCategory.StatusOrOutcome, + Assert.True( + s_addedEnums.TryGetValue(enumType, out EnumCategory category) && + category == EnumCategory.StatusOrOutcome, $"{enumType} is pending but not classified as {EnumCategory.StatusOrOutcome}."); Assert.False(string.IsNullOrWhiteSpace(owner)); - string? zero = declared.TryGetValue(enumType, out SortedDictionary? members) ? ZeroMember(members) : null; + string? zero = declared.TryGetValue(enumType, out SortedDictionary? members) + ? ZeroMember(members) + : null; Assert.True(zero is not null && s_successLikeNames.Contains(zero), $"{enumType} no longer starts with a success-like member ({zero ?? "no zero member"}): {owner} fixed it, so remove it from {nameof(s_pendingZeroValueFixes)}."); } @@ -209,7 +211,8 @@ private static Dictionary> EnumMembers( { foreach (string line in lines(library)) { - if (PublicApiDeclarations.TryParseEnumMember(line, out string enumType, out string member, out long value)) + if (PublicApiDeclarations.TryParseEnumMember(line, out string enumType, out string member, + out long value)) { if (!enums.TryGetValue(enumType, out SortedDictionary? members)) { @@ -242,4 +245,13 @@ private static string SimpleName(string typeName) { return typeName[(typeName.LastIndexOf('.') + 1)..]; } + + private enum EnumCategory + { + Unknown = 0, + StatusOrOutcome, + Policy, + ReasonOrEvidence, + AbiDecision + } } diff --git a/tests/CheatEngine.SDK.Repository.Tests/PublicApi/PublicApiDeclarations.cs b/tests/CheatEngine.SDK.Repository.Tests/PublicApi/PublicApiDeclarations.cs index 3c431de0..cae772db 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/PublicApi/PublicApiDeclarations.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/PublicApi/PublicApiDeclarations.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -140,7 +141,7 @@ public static bool TryParseEnumMember(string line, out string enumType, out stri enumType = match.Groups["type"].Value; member = match.Groups["member"].Value; - value = long.Parse(match.Groups["value"].Value, System.Globalization.CultureInfo.InvariantCulture); + value = long.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture); return true; } @@ -185,10 +186,10 @@ private static string SimpleName(string type) return type[(type.LastIndexOf('.') + 1)..]; } - [GeneratedRegex(@"`+\d+", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"`+\d+", RegexOptions.CultureInvariant, 1000)] private static partial Regex GenericArity(); [GeneratedRegex(@"^(?[\w.]+)\.(?\w+) = (?-?\d+) -> \k$", RegexOptions.CultureInvariant, - matchTimeoutMilliseconds: 1000)] + 1000)] private static partial Regex EnumMemberLine(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/README.md b/tests/CheatEngine.SDK.Repository.Tests/README.md index 64b32a5e..8c7f97a1 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/README.md +++ b/tests/CheatEngine.SDK.Repository.Tests/README.md @@ -14,21 +14,21 @@ workflow's own "submit" step text against a mocked `gh`, offline. ## How it works -| Folder | Content | -|--------------------|--------------------------------------------------------------------------------------------------------| -| `Infrastructure/` | `RepositoryRoot` finds `CheatEngine.SDK.slnx` above the test binaries and enumerates source files. | -| `Solution/` | `SolutionInventoryTests` compares the projects on disk with the projects listed in the solution; `QualificationHarnessShapeTests` checks the shape of the two qualification-harness projects. | -| `Toolchain/` | `ToolchainPinTests` reads `global.json`, `Directory.Build.props` and `Directory.Solution.targets`: exact SDK, analysis-level pin, NuGet audit policy. | -| `LockFiles/` | `LockFileTests` mirror the structural checks a locked restore relies on, over the committed `packages.lock.json` files. | -| `PublicApi/` | `PublicApiFileTests` and `EnumContractTests` check the shape of every shipping library's PublicAPI files and the classification of enums added since 1.0.0. | -| `Abi/` | `ClassicSlotRegistryDocumentTests` checks the committed classic slot registry `Abi/TestData/classic-slot-registry.json` (test-owned data, never a top-level `docs/` folder); `NativeAotProfileDocumentTests` checks the NativeAOT restrictions published in `libs/CheatEngine.SDK.Abi/README.md`; `AbiRouteSeparationTests` keeps the managed and classic exported-function tables apart in `libs/**`. | -| `SourceScanning/` | `CSharpCode` blanks the comments and literals of C# source read as text, and `TestMethodTraits` reads the `[Trait("Qualification", …)]` attributes above a named test method, both for the rules in `Abi/` and `LuaBridge/`. | -| `LuaBridge/` | `ProtectedOperationCatalogTests` checks `libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json` against the LF-pinned C bridge source and the failure probe; `LuaInteropPrimitiveMatrixDocumentTests` checks the committed primitive matrix `LuaBridge/TestData/lua-interop-primitives.json` and the raw `LuaApi` uses of `libs/**`. | -| `Workflows/` | `WorkflowContractTests` parse `.github/workflows/*.yml` and the composite actions with YamlDotNet and freeze the CI contract (job ids, the Gate, lint, format, restore, supply-chain jobs). | -| `Release/` | `ReleaseWorkflowContractTests` reads `.github/workflows/release.yml`: the draft-first job chain, tag guards, write scopes, trusted publishing placement and the reserved artifact names. | -| `Governance/` | `GovernanceWorkflowTests` freeze CodeQL, Scorecard, the online zizmor run and dependency submission; `DependabotConfigurationTests` checks `.github/dependabot.yml`; `GovernanceDocumentTests` checks `SECURITY.md`, `CODE_OF_CONDUCT.md`, `.github/CODEOWNERS` and the issue forms. | -| `Generation/` | `GeneratorInventoryTests` pins the reviewed source generator projects, keeps the extended analyzer rules on every Roslyn component and refuses a local Cheat Engine path in their sources. | -| `Ownership/` | `OwnershipPolicyTests` scans the shipping sources: no finalizer, and no binding of Cheat Engine's global that deletes every registered symbol. | +| Folder | Content | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Infrastructure/` | `RepositoryRoot` finds `CheatEngine.SDK.slnx` above the test binaries and enumerates source files. | +| `Solution/` | `SolutionInventoryTests` compares the projects on disk with the projects listed in the solution; `QualificationHarnessShapeTests` checks the shape of the two qualification-harness projects. | +| `Toolchain/` | `ToolchainPinTests` reads `global.json`, `Directory.Build.props` and `Directory.Solution.targets`: exact SDK, analysis-level pin, NuGet audit policy. | +| `LockFiles/` | `LockFileTests` mirror the structural checks a locked restore relies on, over the committed `packages.lock.json` files. | +| `PublicApi/` | `PublicApiFileTests` and `EnumContractTests` check the shape of every shipping library's PublicAPI files and the classification of enums added since 1.0.0. | +| `Abi/` | `ClassicSlotRegistryDocumentTests` checks the committed classic slot registry `Abi/TestData/classic-slot-registry.json` (test-owned data, never a top-level `docs/` folder); `NativeAotProfileDocumentTests` checks the NativeAOT restrictions published in `libs/CheatEngine.SDK.Abi/README.md`; `AbiRouteSeparationTests` keeps the managed and classic exported-function tables apart in `libs/**`. | +| `SourceScanning/` | `CSharpCode` blanks the comments and literals of C# source read as text, and `TestMethodTraits` reads the `[Trait("Qualification", …)]` attributes above a named test method, both for the rules in `Abi/` and `LuaBridge/`. | +| `LuaBridge/` | `ProtectedOperationCatalogTests` checks `libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json` against the LF-pinned C bridge source and the failure probe; `LuaInteropPrimitiveMatrixDocumentTests` checks the committed primitive matrix `LuaBridge/TestData/lua-interop-primitives.json` and the raw `LuaApi` uses of `libs/**`. | +| `Workflows/` | `WorkflowContractTests` parse `.github/workflows/*.yml` and the composite actions with YamlDotNet and freeze the CI contract (job ids, the Gate, lint, format, restore, supply-chain jobs). | +| `Release/` | `ReleaseWorkflowContractTests` reads `.github/workflows/release.yml`: the draft-first job chain, tag guards, write scopes, trusted publishing placement and the reserved artifact names. | +| `Governance/` | `GovernanceWorkflowTests` freeze CodeQL, Scorecard, the online zizmor run and dependency submission; `DependabotConfigurationTests` checks `.github/dependabot.yml`; `GovernanceDocumentTests` checks `SECURITY.md`, `CODE_OF_CONDUCT.md`, `.github/CODEOWNERS` and the issue forms. | +| `Generation/` | `GeneratorInventoryTests` pins the reviewed source generator projects, keeps the extended analyzer rules on every Roslyn component and refuses a local Cheat Engine path in their sources. | +| `Ownership/` | `OwnershipPolicyTests` scans the shipping sources: no finalizer, and no binding of Cheat Engine's global that deletes every registered symbol. | ## Promise @@ -59,8 +59,8 @@ workflow's own "submit" step text against a mocked `gh`, offline. `Every_enum_added_after_1_0_0_is_classified`, `Status_and_outcome_enums_added_after_1_0_0_do_not_default_to_success`, `Pending_zero_value_fixes_are_still_needed`). - Every project, inside or outside the solution, has a lock file in NuGet's version 2 format, ending as NuGet writes it; - Native AOT projects lock their runtime-specific ILCompiler package; no lock resolves a CheatEngine.* package from a feed - (`Every_project_has_a_committed_lock_file`, `Lock_files_parse_and_declare_a_supported_format_version`, + Native AOT projects lock their runtime-specific ILCompiler package; no lock resolves a CheatEngine.* package from a + feed (`Every_project_has_a_committed_lock_file`, `Lock_files_parse_and_declare_a_supported_format_version`, `Every_lock_file_is_version_2_because_every_project_uses_central_package_management`, `Version_1_lock_files_hold_no_central_transitive_entries`, `Native_aot_projects_lock_the_win_x64_ilcompiler_packages`, `No_lock_file_resolves_a_cheatengine_package`, `Lock_files_end_without_a_final_newline_as_nuget_writes_them`). @@ -77,27 +77,34 @@ workflow's own "submit" step text against a mocked `gh`, offline. `Pull_request_and_main_callers_request_sonar_and_the_release_run_never_does`). - No workflow listens to `pull_request_target` or `merge_group`, the pull-request workflow filters no path, main keeps every run and pull requests cancel superseded ones - (`No_workflow_uses_pull_request_target_or_a_merge_group_trigger`, `Pull_request_and_policy_workflows_have_no_path_filters`, - `Main_ci_runs_every_push_to_main_without_a_concurrency_group`, `Pull_request_ci_skips_drafts_and_cancels_superseded_runs`). + (`No_workflow_uses_pull_request_target_or_a_merge_group_trigger`, + `Pull_request_and_policy_workflows_have_no_path_filters`, + `Main_ci_runs_every_push_to_main_without_a_concurrency_group`, + `Pull_request_ci_skips_drafts_and_cancels_superseded_runs`). - Every job runs on `windows-2025` or `ubuntu-24.04` with a timeout, workflows grant read permissions only at the top - level and the pipeline never elevates, every remote action is pinned to a commit with its version, every checkout drops + level and the pipeline never elevates, every remote action is pinned to a commit with its version, every checkout + drops its credentials, every native command checks its exit code, no run script interpolates an expression, and the pipeline scripts run in `pwsh` (`Every_job_has_a_timeout_and_a_pinned_runner_label`, `Workflows_grant_only_read_permissions_at_the_top_level`, `Pipeline_jobs_never_elevate_permissions`, - `Every_remote_action_is_pinned_to_a_full_sha_with_a_version_comment`, `Every_checkout_disables_credential_persistence`, + `Every_remote_action_is_pinned_to_a_full_sha_with_a_version_comment`, + `Every_checkout_disables_credential_persistence`, `Every_native_command_in_a_workflow_script_checks_its_exit_code`, `No_run_script_interpolates_an_expression`, `Pipeline_workflows_and_composite_actions_run_scripts_in_pwsh`). - Every dotnet job installs the pinned SDK through the composite action, every restore is locked, and no job reachable from a release, Sonar or CodeQL run uses a package cache (`Every_dotnet_job_uses_the_composite_setup_action`, `Composite_setup_restores_in_locked_mode`, `Every_restore_in_the_pipeline_is_locked`, - `Release_reachable_workflows_never_enable_a_package_cache`, `Sonar_restores_locked_from_nuget_org_before_the_scanner_begins`). + `Release_reachable_workflows_never_enable_a_package_cache`, + `Sonar_restores_locked_from_nuget_org_before_the_scanner_begins`). - The Release leg packs before it tests and hands the exact nupkg to the packaging tests, with an inlined pre-publish sanity check (one package, its nuspec identity, the embedded SBOM and the CI-built native bridge); the Debug leg excludes packaging tests by trait, never by skip; every module runs once with hang and crash dumps well inside the job timeout (`Release_leg_packs_before_testing_and_exports_the_exact_nupkg`, - `Debug_leg_excludes_packaging_tests_by_trait_never_by_skip`, `Test_step_runs_every_module_once_with_the_contract_options`, + `Debug_leg_excludes_packaging_tests_by_trait_never_by_skip`, + `Test_step_runs_every_module_once_with_the_contract_options`, `Every_test_module_references_the_extensions_the_test_step_uses`, - `Hang_dump_timeout_is_well_below_the_build_test_job_timeout`, `Build_test_runs_both_configurations_without_fail_fast`). + `Hang_dump_timeout_is_well_below_the_build_test_job_timeout`, + `Build_test_runs_both_configurations_without_fail_fast`). - Artifacts use the reserved names and retentions only, binary logs and dumps are uploaded on failure only and never from Sonar or release runs, jobs that version a package fetch full history, the Native AOT probes are published, and the live probe is compiled exactly once and never shipped (`Every_uploaded_artifact_name_is_reserved`, @@ -115,7 +122,8 @@ workflow's own "submit" step text against a mocked `gh`, offline. - No workflow runs a local qualification runner or generates ApiCompat suppressions (`No_workflow_references_the_local_qualification_runner`, `No_workflow_passes_ApiCompatGenerateSuppressionFile`). - The release workflow is draft-first (`verify → ci → attest → draft-release → publish → verify-publication → - finalize-release`), runs for `v*.*.*` tags and manual dry runs without cancelling a run in progress, and calls `ci.yml` + finalize-release`), runs for `v*.*.*` tags and manual dry runs without cancelling a run in progress, and calls + `ci.yml` with the tag version, a 90-day retention and no Sonar (`Release_jobs_form_the_draft_first_chain`, `Release_runs_for_version_tags_and_manual_dry_runs_without_cancelling`, `Release_calls_ci_with_the_tag_version_ninety_day_retention_and_no_sonar`). diff --git a/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflow.cs b/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflow.cs index ba38e34c..db2e8352 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflow.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflow.cs @@ -154,13 +154,17 @@ public static string Normalize(string? value) /// A child mapping, or null. public static YamlMappingNode? Mapping(YamlMappingNode node, string key) { - return node.Children.TryGetValue(new YamlScalarNode(key), out YamlNode? value) ? value as YamlMappingNode : null; + return node.Children.TryGetValue(new YamlScalarNode(key), out YamlNode? value) + ? value as YamlMappingNode + : null; } /// A child sequence, or null. public static YamlSequenceNode? Sequence(YamlMappingNode node, string key) { - return node.Children.TryGetValue(new YamlScalarNode(key), out YamlNode? value) ? value as YamlSequenceNode : null; + return node.Children.TryGetValue(new YamlScalarNode(key), out YamlNode? value) + ? value as YamlSequenceNode + : null; } /// The keys of a mapping, in order. @@ -187,6 +191,6 @@ public static List ScalarValues(YamlSequenceNode sequence) return values; } - [GeneratedRegex(@"\s+", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\s+", RegexOptions.CultureInvariant, 1000)] private static partial Regex Whitespace(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflowContractTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflowContractTests.cs index 048717fb..07160c30 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflowContractTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Release/ReleaseWorkflowContractTests.cs @@ -6,8 +6,12 @@ namespace CheatEngine.SDK.Repository.Tests.Release; /// /// The release workflow is draft-first and compatible with immutable releases (shared contract 1.11, audit ch.21 -/// "Le tuple a qualifier", Checkpoint F): verify → ci → attest → draft-release → publish → verify-publication → -/// finalize-release. Attestations and every asset exist before the draft is published, publication jobs run only +/// "Le tuple a qualifier", Checkpoint F): +/// +/// verify → ci → attest → draft-release → publish → verify-publication → +/// finalize-release +/// +/// . Attestations and every asset exist before the draft is published, publication jobs run only /// for version tags of this repository, the only write tokens live in the jobs that need them, NuGet trusted /// publishing stays in the publish job with environment nuget, and nothing is ever uploaded to a /// published release. These tests read the committed YAML only. @@ -31,9 +35,11 @@ private static readonly (string Id, string Name, string[] Needs)[] s_chain = ("finalize-release", "Publish GitHub release", ["verify", "attest", "verify-publication"]) ]; - private static readonly string[] s_publicationJobs = ["draft-release", "publish", "verify-publication", "finalize-release"]; + private static readonly string[] s_publicationJobs = + ["draft-release", "publish", "verify-publication", "finalize-release"]; - private static readonly HashSet s_runnerLabels = new(StringComparer.Ordinal) { "windows-2025", "ubuntu-24.04" }; + private static readonly HashSet s_runnerLabels = + new(StringComparer.Ordinal) { "windows-2025", "ubuntu-24.04" }; [Fact] public void Release_runs_for_version_tags_and_manual_dry_runs_without_cancelling() @@ -50,7 +56,8 @@ public void Release_runs_for_version_tags_and_manual_dry_runs_without_cancelling YamlMappingNode concurrency = ReleaseWorkflow.Mapping(workflow.Root, "concurrency")!; Assert.Equal("${{ github.workflow }}-${{ github.ref }}", ReleaseWorkflow.Scalar(concurrency, "group")); Assert.Equal("false", ReleaseWorkflow.Scalar(concurrency, "cancel-in-progress")); - Assert.Equal("read", ReleaseWorkflow.Scalar(ReleaseWorkflow.Mapping(workflow.Root, "permissions")!, "contents")); + Assert.Equal("read", + ReleaseWorkflow.Scalar(ReleaseWorkflow.Mapping(workflow.Root, "permissions")!, "contents")); Assert.Single(ReleaseWorkflow.Keys(ReleaseWorkflow.Mapping(workflow.Root, "permissions")!)); } @@ -78,11 +85,13 @@ public void Publication_jobs_run_only_for_tags_of_this_repository() string guard = ReleaseWorkflow.Normalize(ReleaseWorkflow.Scalar(workflow.Job(id), "if")); if (s_publicationJobs.Contains(id, StringComparer.Ordinal)) { - Assert.True(string.Equals(TagGuard, guard, StringComparison.Ordinal), $"Job '{id}' must run only when '{TagGuard}', not '{guard}'."); + Assert.True(string.Equals(TagGuard, guard, StringComparison.Ordinal), + $"Job '{id}' must run only when '{TagGuard}', not '{guard}'."); } else { - Assert.True(guard.Length == 0, $"Job '{id}' runs on dry runs too and must not be conditional ('{guard}')."); + Assert.True(guard.Length == 0, + $"Job '{id}' runs on dry runs too and must not be conditional ('{guard}')."); } } @@ -93,7 +102,8 @@ public void Publication_jobs_run_only_for_tags_of_this_repository() foreach (YamlMappingNode step in ReleaseWorkflow.Steps(attest)) { bool signsOrVerifies = attestations.Contains(step) - || (ReleaseWorkflow.Scalar(step, "run") ?? "").Contains("gh attestation verify", StringComparison.Ordinal); + || (ReleaseWorkflow.Scalar(step, "run") ?? "").Contains("gh attestation verify", + StringComparison.Ordinal); if (signsOrVerifies) { Assert.Equal(TagGuard, ReleaseWorkflow.Normalize(ReleaseWorkflow.Scalar(step, "if"))); @@ -132,13 +142,16 @@ public void Only_the_publish_job_uses_the_nuget_environment_and_nuget_login() { YamlMappingNode job = workflow.Job(id); bool isPublish = string.Equals(id, "publish", StringComparison.Ordinal); - YamlNode? environment = job.Children.TryGetValue(new YamlScalarNode("environment"), out YamlNode? value) ? value : null; + YamlNode? environment = job.Children.TryGetValue(new YamlScalarNode("environment"), out YamlNode? value) + ? value + : null; List logins = ReleaseWorkflow.StepsUsing(job, "NuGet/login@"); bool pushes = ReleaseWorkflow.RunText(job).Contains("dotnet nuget push", StringComparison.Ordinal); if (!isPublish) { Assert.True(environment is null, $"Job '{id}' uses an environment; only 'publish' may use 'nuget'."); - Assert.True(logins.Count == 0 && !pushes, $"Job '{id}' logs in to or pushes to NuGet; only 'publish' may."); + Assert.True(logins.Count == 0 && !pushes, + $"Job '{id}' logs in to or pushes to NuGet; only 'publish' may."); continue; } @@ -150,7 +163,8 @@ public void Only_the_publish_job_uses_the_nuget_environment_and_nuget_login() List steps = ReleaseWorkflow.Steps(job); int loginIndex = steps.IndexOf(login); Assert.True(loginIndex + 1 < steps.Count, "The NuGet login must be followed by the push."); - Assert.Contains("dotnet nuget push", ReleaseWorkflow.Scalar(steps[loginIndex + 1], "run") ?? "", StringComparison.Ordinal); + Assert.Contains("dotnet nuget push", ReleaseWorkflow.Scalar(steps[loginIndex + 1], "run") ?? "", + StringComparison.Ordinal); Assert.Equal("${{ steps.login.outputs.NUGET_API_KEY }}", ReleaseWorkflow.Scalar(ReleaseWorkflow.Mapping(steps[loginIndex + 1], "env")!, "NUGET_API_KEY")); } @@ -176,7 +190,9 @@ public void Contents_write_is_limited_to_draft_release_and_finalize_release() { foreach ((string scope, string value) in ReleaseWorkflow.Permissions(workflow.Job(id))) { - Assert.True(!string.Equals(value, "write", StringComparison.Ordinal) || scope is "contents" or "id-token" or "attestations", + Assert.True( + !string.Equals(value, "write", StringComparison.Ordinal) || + scope is "contents" or "id-token" or "attestations", $"Job '{id}' grants '{scope}: write'."); } } @@ -192,12 +208,16 @@ public void Release_is_created_as_a_draft_and_published_only_by_finalize() string run = ReleaseWorkflow.RunText(workflow.Job(id)); bool creates = run.Contains("gh release create", StringComparison.Ordinal); bool edits = run.Contains("gh release edit", StringComparison.Ordinal); - bool clobbers = run.Contains("--clobber", StringComparison.Ordinal) && run.Contains("gh release upload", StringComparison.Ordinal); + bool clobbers = run.Contains("--clobber", StringComparison.Ordinal) && + run.Contains("gh release upload", StringComparison.Ordinal); bool publishes = run.Contains("--draft=false", StringComparison.Ordinal); Assert.True(!creates || id is "draft-release", $"Job '{id}' creates a release; only 'draft-release' may."); - Assert.True(!edits || id is "finalize-release", $"Job '{id}' edits a release; only 'finalize-release' may."); - Assert.True(!publishes || id is "finalize-release", $"Job '{id}' publishes the release; only 'finalize-release' may."); - Assert.True(!clobbers || id is "draft-release" or "finalize-release", $"Job '{id}' replaces a release asset."); + Assert.True(!edits || id is "finalize-release", + $"Job '{id}' edits a release; only 'finalize-release' may."); + Assert.True(!publishes || id is "finalize-release", + $"Job '{id}' publishes the release; only 'finalize-release' may."); + Assert.True(!clobbers || id is "draft-release" or "finalize-release", + $"Job '{id}' replaces a release asset."); Assert.DoesNotContain("gh release delete", run, StringComparison.Ordinal); } @@ -216,8 +236,10 @@ public void Release_is_created_as_a_draft_and_published_only_by_finalize() // Publishing runs only while the release is still a draft. YamlMappingNode publishStep = Assert.Single(ReleaseWorkflow.Steps(workflow.Job("finalize-release")), - static step => (ReleaseWorkflow.Scalar(step, "run") ?? "").Contains("--draft=false", StringComparison.Ordinal)); - Assert.Equal("steps.state.outputs.draft == 'true'", ReleaseWorkflow.Normalize(ReleaseWorkflow.Scalar(publishStep, "if"))); + static step => + (ReleaseWorkflow.Scalar(step, "run") ?? "").Contains("--draft=false", StringComparison.Ordinal)); + Assert.Equal("steps.state.outputs.draft == 'true'", + ReleaseWorkflow.Normalize(ReleaseWorkflow.Scalar(publishStep, "if"))); } [Fact] @@ -238,7 +260,9 @@ public void Publication_is_verified_on_nuget_org_before_the_release_is_published // The publish job pushes only the file SHA256SUMS of the draft lists. string publish = ReleaseWorkflow.RunText(workflow.Job("publish")); - Assert.True(publish.IndexOf("SHA256SUMS", StringComparison.Ordinal) < publish.IndexOf("dotnet nuget push", StringComparison.Ordinal), + Assert.True( + publish.IndexOf("SHA256SUMS", StringComparison.Ordinal) < + publish.IndexOf("dotnet nuget push", StringComparison.Ordinal), "The publish job must check the package against SHA256SUMS before pushing it."); } @@ -271,7 +295,8 @@ public void Release_jobs_have_a_timeout_and_a_pinned_runner() string? runsOn = ReleaseWorkflow.Scalar(job, "runs-on"); Assert.True(runsOn is not null && s_runnerLabels.Contains(runsOn), $"Job '{id}' runs on '{runsOn}'."); - Assert.True(int.TryParse(ReleaseWorkflow.Scalar(job, "timeout-minutes"), NumberStyles.None, CultureInfo.InvariantCulture, out int minutes) + Assert.True(int.TryParse(ReleaseWorkflow.Scalar(job, "timeout-minutes"), NumberStyles.None, + CultureInfo.InvariantCulture, out int minutes) && minutes is > 0 and <= 60, $"Job '{id}' needs a timeout-minutes between 1 and 60."); } @@ -295,7 +320,8 @@ public void Release_uploads_only_reserved_artifact_names() // The nupkg travels only in ci.yml's nuget-package artifact: attestation-bundles never carries a second copy. Assert.Equal(["release-notes", "attestation-bundles"], uploaded); - YamlMappingNode bundles = Assert.Single(ReleaseWorkflow.StepsUsing(workflow.Job("attest"), "actions/upload-artifact@")); + YamlMappingNode bundles = + Assert.Single(ReleaseWorkflow.StepsUsing(workflow.Job("attest"), "actions/upload-artifact@")); Assert.Contains("!artifacts/release/*.nupkg", ReleaseWorkflow.With(bundles, "path"), StringComparison.Ordinal); } @@ -310,7 +336,8 @@ public void Release_jobs_that_run_dotnet_install_the_pinned_sdk_and_never_cache_ Assert.Empty(ReleaseWorkflow.StepsUsing(job, "actions/cache")); foreach (YamlMappingNode setup in ReleaseWorkflow.StepsUsing(job, "./.github/actions/setup-dotnet")) { - Assert.True(ReleaseWorkflow.With(setup, "cache") is null or "false", $"Job '{id}' enables a package cache."); + Assert.True(ReleaseWorkflow.With(setup, "cache") is null or "false", + $"Job '{id}' enables a package cache."); } bool needsDotnet = id is "publish" or "verify-publication"; diff --git a/tests/CheatEngine.SDK.Repository.Tests/Runtime/RuntimeCapabilitySpecTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Runtime/RuntimeCapabilitySpecTests.cs index d56429f6..6cc815a2 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Runtime/RuntimeCapabilitySpecTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Runtime/RuntimeCapabilitySpecTests.cs @@ -27,8 +27,17 @@ public sealed partial class RuntimeCapabilitySpecTests /// private static readonly SortedSet s_readOnlyGlobals = new(StringComparer.Ordinal) { - "getCEVersion", "isConnectedToCEServer", "targetIs64Bit", "targetIsX86", "targetIsArm", "targetIsAndroid", - "getABI", "getPointerSize", "getCheatEngineFileVersion", "getSystemArchitecture", "cheatEngineIs64Bit", + "getCEVersion", + "isConnectedToCEServer", + "targetIs64Bit", + "targetIsX86", + "targetIsArm", + "targetIsAndroid", + "getABI", + "getPointerSize", + "getCheatEngineFileVersion", + "getSystemArchitecture", + "cheatEngineIs64Bit", "getOperatingSystem" }; @@ -113,12 +122,15 @@ public void target_backend_members_are_the_readme_backend_rows_and_only_local_pr SortedSet rows = new(StringComparer.Ordinal); List qualified = []; - foreach (string[] cells in BackendTableRows(File.ReadAllLines(Path.Combine(RepositoryRoot.Path, EngineReadmePath)))) + foreach (string[] cells in BackendTableRows( + File.ReadAllLines(Path.Combine(RepositoryRoot.Path, EngineReadmePath)))) { string backend = cells[0].Trim('`'); Assert.True(rows.Add(backend), $"Duplicate backend row {backend}."); string verdict = cells[^1]; - Assert.True(string.Equals(verdict, "no", StringComparison.Ordinal) || verdict.StartsWith("yes, ", StringComparison.Ordinal), + Assert.True( + string.Equals(verdict, "no", StringComparison.Ordinal) || + verdict.StartsWith("yes, ", StringComparison.Ordinal), $"The qualified cell of {backend} is neither 'no' nor 'yes, ': {verdict}"); if (verdict.StartsWith("yes", StringComparison.Ordinal)) { diff --git a/tests/CheatEngine.SDK.Repository.Tests/Solution/QualificationHarnessShapeTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Solution/QualificationHarnessShapeTests.cs index d5cee953..e6f46f1d 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Solution/QualificationHarnessShapeTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Solution/QualificationHarnessShapeTests.cs @@ -10,7 +10,9 @@ namespace CheatEngine.SDK.Repository.Tests.Solution; public sealed class QualificationHarnessShapeTests { private const string LiveProbe = "tests/CheatEngine.SDK.LiveProbe/CheatEngine.SDK.LiveProbe.csproj"; - private const string Target = "tests/CheatEngine.SDK.QualificationTarget/CheatEngine.SDK.QualificationTarget.csproj"; + + private const string Target = + "tests/CheatEngine.SDK.QualificationTarget/CheatEngine.SDK.QualificationTarget.csproj"; [Fact] public void LiveProbe_is_in_the_solution_as_an_x64_dynamic_loading_plugin_that_never_packs() @@ -35,7 +37,8 @@ public void QualificationTarget_is_in_the_solution_and_publishes_native_aot_for_ Assert.Equal("Exe", Property(project, "OutputType")); Assert.Equal("true", Property(project, "PublishAot")); Assert.Equal(["win-x64", "win-x86"], - (Property(project, "RuntimeIdentifiers") ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries)); + (Property(project, "RuntimeIdentifiers") ?? string.Empty).Split(';', + StringSplitOptions.RemoveEmptyEntries)); Assert.Null(Property(project, "RuntimeIdentifier")); Assert.True(File.Exists(Absolute("tests/CheatEngine.SDK.QualificationTarget/README.md"))); } @@ -73,7 +76,8 @@ public void QualificationTarget_references_no_SDK_project() private static XElement Project(string path) { - return XDocument.Load(Absolute(path)).Root ?? throw new InvalidOperationException(path + " has no root element."); + return XDocument.Load(Absolute(path)).Root ?? + throw new InvalidOperationException(path + " has no root element."); } private static string? Property(XElement project, string name) diff --git a/tests/CheatEngine.SDK.Repository.Tests/Toolchain/ToolchainPinTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Toolchain/ToolchainPinTests.cs index 5066ab8e..6635ed33 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Toolchain/ToolchainPinTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Toolchain/ToolchainPinTests.cs @@ -240,9 +240,9 @@ private static JsonElement ReadGlobalJsonSdk() return document.RootElement.GetProperty("sdk").Clone(); } - [GeneratedRegex(@"^\d+\.\d+\.\d{3}$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\d+\.\d+\.\d{3}$", RegexOptions.CultureInvariant, 1000)] private static partial Regex ExactSdkVersion(); - [GeneratedRegex(@"^\d+\.\d+-recommended$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\d+\.\d+-recommended$", RegexOptions.CultureInvariant, 1000)] private static partial Regex PinnedAnalysisLevel(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContract.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContract.cs index 6bcb54e6..3f56aaa4 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContract.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContract.cs @@ -48,7 +48,8 @@ internal static class WorkflowContract public const string BinlogRetention = "5"; /// The only runner labels a job may use (never a floating -latest label). - public static readonly HashSet RunnerLabels = new(StringComparer.Ordinal) { "windows-2025", "ubuntu-24.04" }; + public static readonly HashSet + RunnerLabels = new(StringComparer.Ordinal) { "windows-2025", "ubuntu-24.04" }; /// The workflows that call ci.yml and so produce "CI / Gate". public static readonly string[] Callers = ["pull-request-ci.yml", "main-ci.yml", "release.yml"]; diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.BuildTest.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.BuildTest.cs index c167d977..dbb00d1e 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.BuildTest.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.BuildTest.cs @@ -32,12 +32,14 @@ public void Build_test_runs_both_configurations_without_fail_fast() YamlMappingNode strategy = Assert.IsType(WorkflowFile.Mapping(job.Node, "strategy")); Assert.Equal("false", WorkflowFile.Scalar(strategy, "fail-fast")); YamlMappingNode matrix = Assert.IsType(WorkflowFile.Mapping(strategy, "matrix")); - Assert.Equal(["Debug", "Release"], WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(matrix, "configuration")))); + Assert.Equal(["Debug", "Release"], + WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(matrix, "configuration")))); Assert.Equal(["native", "native-host-emulator"], job.Needs()); // One build of the whole solution per leg, logged for failure analysis; every later step reuses it. string build = WorkflowFile.Scalar(job.Step("Build"), "run") ?? ""; - Assert.Contains("dotnet build CheatEngine.SDK.slnx -c $env:CONFIGURATION --no-restore", build, StringComparison.Ordinal); + Assert.Contains("dotnet build CheatEngine.SDK.slnx -c $env:CONFIGURATION --no-restore", build, + StringComparison.Ordinal); Assert.Contains("\"-bl:artifacts/logs/build-test-$env:CONFIGURATION.binlog\"", build, StringComparison.Ordinal); } @@ -58,7 +60,8 @@ public void Release_leg_packs_before_testing_and_exports_the_exact_nupkg() Assert.Equal("matrix.configuration == 'Release'", WorkflowFile.Scalar(packStep, "if")); Assert.Equal("${{ inputs.package-version }}", WorkflowJob.Env(packStep, "PACKAGE_VERSION")); string packRun = WorkflowFile.Scalar(packStep, "run") ?? ""; - Assert.Contains("dotnet pack src/CheatEngine.SDK -c Release --no-restore -o artifacts/nuget -bl:artifacts/logs/pack-Release.binlog", + Assert.Contains( + "dotnet pack src/CheatEngine.SDK -c Release --no-restore -o artifacts/nuget -bl:artifacts/logs/pack-Release.binlog", packRun, StringComparison.Ordinal); // The packaging tests consume the packed file itself, in the Release leg only, and log its SHA-256. @@ -77,7 +80,8 @@ public void Release_leg_packs_before_testing_and_exports_the_exact_nupkg() Assert.Contains("'_manifest/spdx_2.2/manifest.spdx.json'", packRun, StringComparison.Ordinal); Assert.Contains("\"nupkg=$($package.FullName)\"", packRun, StringComparison.Ordinal); Assert.Contains("\"sha256=$sha256\"", packRun, StringComparison.Ordinal); - Assert.Contains("$package.Name -cne \"CheatEngine.SDK.$($env:PACKAGE_VERSION).nupkg\"", packRun, StringComparison.Ordinal); + Assert.Contains("$package.Name -cne \"CheatEngine.SDK.$($env:PACKAGE_VERSION).nupkg\"", packRun, + StringComparison.Ordinal); Assert.Contains("'build/native/cheatengine-sdk-lua-bridge.dll'", packRun, StringComparison.Ordinal); } @@ -91,14 +95,18 @@ public void Debug_leg_excludes_packaging_tests_by_trait_never_by_skip() int debug = run.IndexOf("if ($env:CONFIGURATION -eq 'Debug') {", StringComparison.Ordinal); int release = run.IndexOf("else {", debug + 1, StringComparison.Ordinal); int failSkips = run.IndexOf("'--fail-skips', 'on'", StringComparison.Ordinal); - int filter = run.IndexOf($"'--filter-not-trait', '{WorkflowContract.PackagingTrait}'", StringComparison.Ordinal); + int filter = run.IndexOf($"'--filter-not-trait', '{WorkflowContract.PackagingTrait}'", + StringComparison.Ordinal); Assert.True(options >= 0 && options < failSkips && failSkips < debug, "--fail-skips on must be a common option of both legs."); Assert.True(debug < filter && filter < release, "The packaging trait filter belongs to the Debug leg only."); // No other way to hide a test: no second filter, no ignored exit code, no retries in the required run. Assert.Equal(1, Occurrences(run, "--filter")); - foreach (string forbidden in new[] { "--ignore-exit-code", "--retry-failed-tests", "TESTINGPLATFORM_EXITCODE_IGNORE" }) + foreach (string forbidden in new[] + { + "--ignore-exit-code", "--retry-failed-tests", "TESTINGPLATFORM_EXITCODE_IGNORE" + }) { Assert.DoesNotContain(forbidden, run, StringComparison.Ordinal); } @@ -119,8 +127,9 @@ public void Test_step_runs_every_module_once_with_the_contract_options() Assert.Contains("dotnet test @options", run, StringComparison.Ordinal); foreach (string option in new[] { - "'--solution', 'CheatEngine.SDK.slnx'", "'--no-build'", "'--results-directory', $env:RESULTS", "'--report-trx'", - "'--report-gh', '--report-gh-groups', 'off'", "'--hangdump', '--hangdump-timeout'", "'--crashdump'", + "'--solution', 'CheatEngine.SDK.slnx'", "'--no-build'", "'--results-directory', $env:RESULTS", + "'--report-trx'", "'--report-gh', '--report-gh-groups', 'off'", + "'--hangdump', '--hangdump-timeout'", "'--crashdump'", "'--coverage', '--coverage-output-format', 'xml'" }) { @@ -140,7 +149,8 @@ public void Every_test_module_references_the_extensions_the_test_step_uses() HashSet referenced = new(StringComparer.Ordinal); foreach (XElement group in props.Descendants("ItemGroup")) { - if (!((string?) group.Attribute("Condition") ?? "").Contains("$(MSBuildProjectName.EndsWith('.Tests'))", StringComparison.Ordinal)) + if (!((string?) group.Attribute("Condition") ?? "").Contains("$(MSBuildProjectName.EndsWith('.Tests'))", + StringComparison.Ordinal)) { continue; } @@ -167,7 +177,8 @@ public void Hang_dump_timeout_is_well_below_the_build_test_job_timeout() Match hang = HangDumpTimeout().Match(run); Assert.True(hang.Success, "The Test step must pass '--hangdump-timeout', 'm'."); int hangMinutes = int.Parse(hang.Groups["minutes"].Value, NumberStyles.None, CultureInfo.InvariantCulture); - int jobMinutes = int.Parse(WorkflowFile.Scalar(job.Node, "timeout-minutes") ?? "0", NumberStyles.None, CultureInfo.InvariantCulture); + int jobMinutes = int.Parse(WorkflowFile.Scalar(job.Node, "timeout-minutes") ?? "0", NumberStyles.None, + CultureInfo.InvariantCulture); // The dump must be written, uploaded and the job reported well before the runner kills it. Assert.True(hangMinutes > 0 && hangMinutes * 2 <= jobMinutes, @@ -188,7 +199,8 @@ public void Every_uploaded_artifact_name_is_reserved() string retention = WorkflowJob.With(step, "retention-days") ?? ""; foreach (string name in ExpandConfiguration(template)) { - Assert.True(names.Add(name), $"{workflow.FileName} uploads '{name}' twice; artifact names are unique per run."); + Assert.True(names.Add(name), + $"{workflow.FileName} uploads '{name}' twice; artifact names are unique per run."); } string? expectedRetention = ExpectedRetention(template); @@ -218,10 +230,13 @@ public void Binlogs_are_uploaded_only_on_failure_and_never_from_sonar_or_release foreach (YamlMappingNode step in job.StepsUsing("actions/upload-artifact@")) { string name = WorkflowJob.With(step, "name") ?? ""; - if (name.StartsWith("binlogs-", StringComparison.Ordinal) || name.StartsWith("test-dumps-", StringComparison.Ordinal)) + if (name.StartsWith("binlogs-", StringComparison.Ordinal) || + name.StartsWith("test-dumps-", StringComparison.Ordinal)) { - Assert.True(mayLogBuilds, $"{job.Location} uploads '{name}'; sonar.yml and release.yml never do."); - Assert.True(string.Equals(WorkflowFile.Scalar(step, "if"), "failure()", StringComparison.Ordinal), + Assert.True(mayLogBuilds, + $"{job.Location} uploads '{name}'; sonar.yml and release.yml never do."); + Assert.True( + string.Equals(WorkflowFile.Scalar(step, "if"), "failure()", StringComparison.Ordinal), $"{job.Location} uploads '{name}' outside 'if: failure()'."); } } @@ -231,7 +246,8 @@ public void Binlogs_are_uploaded_only_on_failure_and_never_from_sonar_or_release // The jobs that build, pack or publish keep their logs for a failed run. WorkflowFile pipeline = Pipeline(); Assert.Single(pipeline.Job(BuildTestJob).StepsUsing("actions/upload-artifact@"), - static step => string.Equals(WorkflowJob.With(step, "name"), "binlogs-build-test-${{ matrix.configuration }}", StringComparison.Ordinal)); + static step => string.Equals(WorkflowJob.With(step, "name"), + "binlogs-build-test-${{ matrix.configuration }}", StringComparison.Ordinal)); Assert.Single(pipeline.Job("aot").StepsUsing("actions/upload-artifact@"), static step => string.Equals(WorkflowJob.With(step, "name"), "binlogs-aot", StringComparison.Ordinal)); Assert.Equal(3, BinaryLogSwitch().Count(pipeline.Job("aot").RunText())); @@ -245,7 +261,8 @@ public void Jobs_that_pack_or_test_fetch_full_history() foreach (WorkflowJob job in workflow.Jobs()) { string run = job.RunText(); - bool packs = VersionedPack().IsMatch(run) || run.Contains("dotnet-sonarscanner", StringComparison.Ordinal); + bool packs = VersionedPack().IsMatch(run) || + run.Contains("dotnet-sonarscanner", StringComparison.Ordinal); bool builds = VersionedBuild().IsMatch(run); if (!packs && !builds) { @@ -254,7 +271,8 @@ public void Jobs_that_pack_or_test_fetch_full_history() // MinVer computes the version from tags and history; a shallow clone packs 0.0.0-alpha.0. YamlMappingNode checkout = Assert.Single(job.StepsUsing("actions/checkout@")); - bool fullHistory = string.Equals(WorkflowJob.With(checkout, "fetch-depth"), "0", StringComparison.Ordinal); + bool fullHistory = string.Equals(WorkflowJob.With(checkout, "fetch-depth"), "0", + StringComparison.Ordinal); bool skipsVersioning = !packs && run.Contains("MinVerSkip=true", StringComparison.Ordinal); Assert.True(fullHistory || skipsVersioning, $"{job.Location} builds, packs, tests or analyses: check out with fetch-depth: 0 (MinVer)."); @@ -280,13 +298,17 @@ public void Aot_job_publishes_the_native_aot_probes() // Publication and inspection only: a NativeAOT publish is never presented as a Cheat Engine load (audit A20-07). string run = aot.RunText(); - Assert.Contains("dotnet publish tests/CheatEngine.SDK.AotProbe/CheatEngine.SDK.AotProbe.csproj -c Release --no-restore", run, + Assert.Contains( + "dotnet publish tests/CheatEngine.SDK.AotProbe/CheatEngine.SDK.AotProbe.csproj -c Release --no-restore", + run, StringComparison.Ordinal); - Assert.Contains("$libraryProject = 'tests/CheatEngine.SDK.NativeAotLibraryProbe/CheatEngine.SDK.NativeAotLibraryProbe.csproj'", + Assert.Contains( + "$libraryProject = 'tests/CheatEngine.SDK.NativeAotLibraryProbe/CheatEngine.SDK.NativeAotLibraryProbe.csproj'", run, StringComparison.Ordinal); Assert.Contains("dotnet publish $libraryProject -c Release --no-restore", run, StringComparison.Ordinal); Assert.Contains("& $harness --analyze $library", run, StringComparison.Ordinal); - Assert.Contains("lua-protection-bridge", WorkflowJob.With(Assert.Single(aot.StepsUsing("actions/download-artifact@")), "name"), + Assert.Contains("lua-protection-bridge", + WorkflowJob.With(Assert.Single(aot.StepsUsing("actions/download-artifact@")), "name"), StringComparison.Ordinal); } @@ -321,7 +343,8 @@ public void Live_probe_is_compiled_by_the_ci_solution_build() { foreach (YamlMappingNode upload in pipelineJob.StepsUsing("actions/upload-artifact@")) { - Assert.DoesNotContain("LiveProbe", WorkflowJob.With(upload, "path") ?? "", StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("LiveProbe", WorkflowJob.With(upload, "path") ?? "", + StringComparison.OrdinalIgnoreCase); } } } @@ -398,18 +421,19 @@ private static bool IsReservedWithoutRetention(string template) return WorkflowContract.ReservedArtifacts.TryGetValue(template, out string? retention) && retention is null; } - [GeneratedRegex(@"'--hangdump-timeout',\s*'(?\d+)m'", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"'--hangdump-timeout',\s*'(?\d+)m'", RegexOptions.CultureInvariant, 1000)] private static partial Regex HangDumpTimeout(); - [GeneratedRegex(@"\bdotnet\s+pack\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\bdotnet\s+pack\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex VersionedPack(); - [GeneratedRegex(@"\bdotnet\s+(?:build|test|publish)\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\bdotnet\s+(?:build|test|publish)\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex VersionedBuild(); - [GeneratedRegex(@"(?:^|\s|"")[-/]bl(?::|\s|$)", RegexOptions.Multiline | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"(?:^|\s|"")[-/]bl(?::|\s|$)", RegexOptions.Multiline | RegexOptions.CultureInvariant, 1000)] private static partial Regex BinaryLogSwitch(); - [GeneratedRegex(@"^binlogs-[a-z0-9-]+?(?:-\$\{\{ matrix\.configuration \}\})?$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^binlogs-[a-z0-9-]+?(?:-\$\{\{ matrix\.configuration \}\})?$", RegexOptions.CultureInvariant, + 1000)] private static partial Regex BinlogName(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Gate.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Gate.cs index 3f75b01f..ac13ed51 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Gate.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Gate.cs @@ -1,3 +1,5 @@ +using System.Globalization; + using YamlDotNet.RepresentationModel; namespace CheatEngine.SDK.Repository.Tests.Workflows; @@ -29,7 +31,7 @@ public void Callers_invoke_ci_through_job_ci_named_CI() if (string.Equals(job.Uses, PipelineUses, StringComparison.Ordinal)) { Assert.True(string.Equals(job.Id, WorkflowContract.CallerJobId, StringComparison.Ordinal) && - string.Equals(job.Name, WorkflowContract.CallerJobName, StringComparison.Ordinal), + string.Equals(job.Name, WorkflowContract.CallerJobName, StringComparison.Ordinal), $"{job.Location} calls ci.yml; only a job 'ci' named 'CI' may."); } } @@ -75,7 +77,8 @@ public void Gate_job_is_named_Gate_runs_always_and_has_no_permissions() Assert.Contains("$required = 'skipped'", run, StringComparison.Ordinal); Assert.Contains("if ($result -ne $required)", run, StringComparison.Ordinal); Assert.Contains("| Job | Result | Required | Reason |", run, StringComparison.Ordinal); - Assert.Contains("Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8", run, StringComparison.Ordinal); + Assert.Contains("Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8", run, + StringComparison.Ordinal); Assert.Contains("exit 1", run, StringComparison.Ordinal); } @@ -122,7 +125,7 @@ public void Ci_jobs_match_the_frozen_contract_ids_and_names() Assert.True(jobs.Remove(expected.Id, out WorkflowJob? job), $"ci.yml has no job '{expected.Id}'."); Assert.Equal(expected.Name, job.Name); Assert.Equal(expected.RunsOn, job.RunsOn); - Assert.Equal(expected.TimeoutMinutes?.ToString(System.Globalization.CultureInfo.InvariantCulture), + Assert.Equal(expected.TimeoutMinutes?.ToString(CultureInfo.InvariantCulture), WorkflowFile.Scalar(job.Node, "timeout-minutes")); } @@ -149,7 +152,9 @@ public void Ci_declares_exactly_the_contract_inputs_and_secret() YamlMappingNode secrets = Assert.IsType(WorkflowFile.Mapping(call, "secrets")); Assert.Equal(["SONAR_TOKEN"], WorkflowFile.Keys(secrets)); - Assert.Equal("false", WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(secrets, "SONAR_TOKEN")), "required")); + Assert.Equal("false", + WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(secrets, "SONAR_TOKEN")), + "required")); Assert.False(WorkflowFile.Has(call, "outputs"), "ci.yml has no workflow_call outputs in v1 (contract 1.4)."); // No repository variable steers the pipeline (contract 1.4). @@ -176,7 +181,8 @@ public void Sonar_waits_for_the_quality_gate_outside_push_events() Assert.Equal(["build-test"], sonar.Needs()); Assert.Equal("${{ github.event_name != 'push' }}", WorkflowJob.With(sonar.Node, "wait-quality-gate")); Assert.Equal("${{ secrets.SONAR_TOKEN }}", - WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(sonar.Node, "secrets")), "SONAR_TOKEN")); + WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(sonar.Node, "secrets")), + "SONAR_TOKEN")); WorkflowJob analyze = WorkflowFile.LoadWorkflow(WorkflowContract.Sonar).Job("analyze"); YamlMappingNode env = Assert.IsType(WorkflowFile.Mapping(analyze.Node, "env")); @@ -188,7 +194,8 @@ public void Sonar_waits_for_the_quality_gate_outside_push_events() YamlMappingNode link = analyze.Step("Link the analysis"); Assert.True(analyze.StepIndex("End analysis") < analyze.StepIndex("Link the analysis")); Assert.Equal("${{ !cancelled() }}", WorkflowFile.Scalar(link, "if")); - Assert.Contains("Out-File -FilePath $env:GITHUB_STEP_SUMMARY", WorkflowFile.Scalar(link, "run"), StringComparison.Ordinal); + Assert.Contains("Out-File -FilePath $env:GITHUB_STEP_SUMMARY", WorkflowFile.Scalar(link, "run"), + StringComparison.Ordinal); } [Fact] @@ -200,8 +207,8 @@ public void Sonar_excludes_non_product_trees_from_analysis_and_coverage() HashSet exclusions = new(SonarProperty(begin, "sonar.exclusions").Split(','), StringComparer.Ordinal); foreach (string tree in new[] { - "artifacts/**", "tests/CheatEngine.SDK.QualificationTarget/**", "tests/native-host-emulator/**", "eng/tools/**", - "docs/**" + "artifacts/**", "tests/CheatEngine.SDK.QualificationTarget/**", + "tests/native-host-emulator/**", "eng/tools/**", "docs/**" }) { Assert.True(exclusions.Contains(tree), $"sonar.exclusions must list {tree}."); @@ -209,7 +216,8 @@ public void Sonar_excludes_non_product_trees_from_analysis_and_coverage() // Tests stay analysed (their issues are triaged by rule), but they never count as product coverage. Assert.False(exclusions.Contains("tests/**"), "Tests stay analysed; only their coverage is excluded."); - HashSet coverage = new(SonarProperty(begin, "sonar.coverage.exclusions").Split(','), StringComparer.Ordinal); + HashSet coverage = new(SonarProperty(begin, "sonar.coverage.exclusions").Split(','), + StringComparer.Ordinal); foreach (string tree in new[] { "tests/**", "eng/**", "docs/**" }) { Assert.True(coverage.Contains(tree), $"sonar.coverage.exclusions must list {tree}."); @@ -239,7 +247,8 @@ public void Pull_request_and_policy_workflows_have_no_path_filters() { // pr-policy.yml no longer exists (the PR title/changelog policy engine was removed); pull-request-ci.yml // must always exist. - Assert.False(string.Equals(fileName, "pull-request-ci.yml", StringComparison.Ordinal), $"{fileName} is missing."); + Assert.False(string.Equals(fileName, "pull-request-ci.yml", StringComparison.Ordinal), + $"{fileName} is missing."); continue; } @@ -251,7 +260,8 @@ public void Pull_request_and_policy_workflows_have_no_path_filters() continue; } - Assert.False(WorkflowFile.Has(configuration, "paths") || WorkflowFile.Has(configuration, "paths-ignore"), + Assert.False( + WorkflowFile.Has(configuration, "paths") || WorkflowFile.Has(configuration, "paths-ignore"), $"{fileName} '{trigger}' must not filter paths: its required check must report on every pull request."); } } @@ -264,7 +274,8 @@ public void Main_ci_runs_every_push_to_main_without_a_concurrency_group() Assert.Equal(["push", "workflow_dispatch"], main.Triggers()); YamlMappingNode push = Assert.IsType(main.Trigger("push")); - Assert.Equal(["main"], WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(push, "branches")))); + Assert.Equal(["main"], + WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(push, "branches")))); Assert.False(WorkflowFile.Has(main.Root, "concurrency"), "main-ci.yml keeps every main commit's run: a concurrency group would cancel or drop intermediate runs."); } @@ -278,10 +289,13 @@ public void Pull_request_ci_skips_drafts_and_cancels_superseded_runs() YamlMappingNode trigger = Assert.IsType(pullRequest.Trigger("pull_request")); Assert.Equal(["opened", "synchronize", "reopened", "ready_for_review"], WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(trigger, "types")))); - Assert.Equal("${{ !github.event.pull_request.draft }}", pullRequest.Job(WorkflowContract.CallerJobId).Condition); + Assert.Equal("${{ !github.event.pull_request.draft }}", + pullRequest.Job(WorkflowContract.CallerJobId).Condition); - YamlMappingNode concurrency = Assert.IsType(WorkflowFile.Mapping(pullRequest.Root, "concurrency")); - Assert.Equal("${{ github.workflow }}-${{ github.event.pull_request.number }}", WorkflowFile.Scalar(concurrency, "group")); + YamlMappingNode concurrency = + Assert.IsType(WorkflowFile.Mapping(pullRequest.Root, "concurrency")); + Assert.Equal("${{ github.workflow }}-${{ github.event.pull_request.number }}", + WorkflowFile.Scalar(concurrency, "group")); Assert.Equal("true", WorkflowFile.Scalar(concurrency, "cancel-in-progress")); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Hygiene.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Hygiene.cs index d97272ce..e4a9a711 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Hygiene.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Hygiene.cs @@ -29,7 +29,8 @@ public void Every_job_has_a_timeout_and_a_pinned_runner_label() } string? timeout = WorkflowFile.Scalar(job.Node, "timeout-minutes"); - if (!int.TryParse(timeout, NumberStyles.None, CultureInfo.InvariantCulture, out int minutes) || minutes <= 0) + if (!int.TryParse(timeout, NumberStyles.None, CultureInfo.InvariantCulture, out int minutes) || + minutes <= 0) { violations.Add(new Violation(workflow.FileName, job.Id, $"{job.Location} has no positive timeout-minutes ('{timeout}').")); @@ -45,7 +46,8 @@ public void Workflows_grant_only_read_permissions_at_the_top_level() { foreach (WorkflowFile workflow in WorkflowFile.LoadWorkflows()) { - Assert.True(workflow.Root.Children.TryGetValue(new YamlScalarNode("permissions"), out YamlNode? permissions), + Assert.True( + workflow.Root.Children.TryGetValue(new YamlScalarNode("permissions"), out YamlNode? permissions), $"{workflow.FileName} must declare top-level permissions (contents: read) and elevate per job only."); if (permissions is YamlScalarNode scalar) { @@ -144,7 +146,8 @@ public void Every_checkout_disables_credential_persistence() } checkouts++; - Assert.True(string.Equals(WorkflowJob.With(step, "persist-credentials"), "false", StringComparison.Ordinal), + Assert.True( + string.Equals(WorkflowJob.With(step, "persist-credentials"), "false", StringComparison.Ordinal), $"{file.RelativePath} line {step.Start.Line}: actions/checkout must set persist-credentials: false."); } } @@ -221,7 +224,8 @@ public void Pipeline_workflows_and_composite_actions_run_scripts_in_pwsh() WorkflowFile workflow = WorkflowFile.LoadWorkflow(fileName); YamlMappingNode? defaults = WorkflowFile.Mapping(workflow.Root, "defaults"); YamlMappingNode? run = defaults is null ? null : WorkflowFile.Mapping(defaults, "run"); - Assert.True(run is not null && string.Equals(WorkflowFile.Scalar(run, "shell"), "pwsh", StringComparison.Ordinal), + Assert.True( + run is not null && string.Equals(WorkflowFile.Scalar(run, "shell"), "pwsh", StringComparison.Ordinal), $"{fileName} must declare defaults.run.shell: pwsh."); } @@ -256,18 +260,18 @@ public void No_workflow_passes_ApiCompatGenerateSuppressionFile() { // Suppressions are regenerated by the integrator only; CI must fail on an undeclared break instead. Assert.False(file.Text.Contains("ApiCompatGenerateSuppressionFile", StringComparison.OrdinalIgnoreCase) || - file.Text.Contains("GenerateCompatibilitySuppressionFile", StringComparison.OrdinalIgnoreCase), + file.Text.Contains("GenerateCompatibilitySuppressionFile", StringComparison.OrdinalIgnoreCase), $"{file.RelativePath} generates an ApiCompat suppression file; CI must fail on an undeclared break instead."); } } - [GeneratedRegex(@"^\s*(?:-\s+)?uses:\s*(?\S+)(?.*)$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s*(?:-\s+)?uses:\s*(?\S+)(?.*)$", RegexOptions.CultureInvariant, 1000)] private static partial Regex UsesLine(); [GeneratedRegex(@"^(?[A-Za-z0-9-]+/[A-Za-z0-9._/-]+)@(?[0-9a-f]{40}) # v\d+\.\d+\.\d+\s*$", - RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + RegexOptions.CultureInvariant, 1000)] private static partial Regex PinnedReference(); - [GeneratedRegex(@"^\s*(?:dotnet|xmake|git|gh|tar)\s|^\s*\./|^\s*&\s", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\s*(?:dotnet|xmake|git|gh|tar)\s|^\s*\./|^\s*&\s", RegexOptions.CultureInvariant, 1000)] private static partial Regex NativeInvocation(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Lint.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Lint.cs index 0fe5929e..d46d2ffb 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Lint.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Lint.cs @@ -92,7 +92,8 @@ public void Format_job_verifies_whitespace_without_restore() YamlMappingNode setup = Assert.Single(format.StepsUsing(WorkflowContract.SetupAction)); Assert.Null(WorkflowJob.With(setup, "restore")); string run = format.RunText(); - Assert.Contains("dotnet format whitespace . --folder --verify-no-changes --exclude artifacts", run, StringComparison.Ordinal); + Assert.Contains("dotnet format whitespace . --folder --verify-no-changes --exclude artifacts", run, + StringComparison.Ordinal); Assert.DoesNotContain("dotnet restore", run, StringComparison.Ordinal); Assert.DoesNotContain("dotnet build", run, StringComparison.Ordinal); // Style rules are enforced by the build (EnforceCodeStyleInBuild, warnings as errors), not by this job. @@ -102,12 +103,12 @@ public void Format_job_verifies_whitespace_without_restore() Assert.Contains("end_of_line = crlf", ReadRepositoryText(".editorconfig"), StringComparison.Ordinal); } - [GeneratedRegex(@"#\s*zizmor:\s*ignore\[[^\]]+\](?[^\n]*)", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"#\s*zizmor:\s*ignore\[[^\]]+\](?[^\n]*)", RegexOptions.CultureInvariant, 1000)] private static partial Regex InlineZizmorIgnore(); - [GeneratedRegex(@"^\d+\.\d+\.\d+$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^\d+\.\d+\.\d+$", RegexOptions.CultureInvariant, 1000)] private static partial Regex ExactVersion(); - [GeneratedRegex(@"^[0-9a-f]{64}$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"^[0-9a-f]{64}$", RegexOptions.CultureInvariant, 1000)] private static partial Regex Sha256(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Restore.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Restore.cs index c194d59b..af646a79 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Restore.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Restore.cs @@ -15,12 +15,16 @@ public void Composite_setup_restores_in_locked_mode() WorkflowFile action = SetupAction(); YamlMappingNode inputs = Assert.IsType(WorkflowFile.Mapping(action.Root, "inputs")); - Assert.Equal("false", WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(inputs, "cache")), "default")); - Assert.Equal("", WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(inputs, "restore")), "default")); + Assert.Equal("false", + WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(inputs, "cache")), "default")); + Assert.Equal("", + WorkflowFile.Scalar(Assert.IsType(WorkflowFile.Mapping(inputs, "restore")), "default")); IReadOnlyList steps = action.ActionSteps(); YamlMappingNode install = Assert.Single(steps, - static step => (WorkflowFile.Scalar(step, "uses") ?? "").StartsWith("actions/setup-dotnet@", StringComparison.Ordinal)); + static step => + (WorkflowFile.Scalar(step, "uses") ?? "").StartsWith("actions/setup-dotnet@", + StringComparison.Ordinal)); Assert.Equal("global.json", WorkflowJob.With(install, "global-json-file")); Assert.Equal("${{ inputs.cache }}", WorkflowJob.With(install, "cache")); Assert.Equal("**/packages.lock.json", WorkflowJob.With(install, "cache-dependency-path")); @@ -57,7 +61,8 @@ public void Every_restore_in_the_pipeline_is_locked() } Assert.True(unlocked.Count == 0, - "Restore with --locked-mode, or through the composite action's restore input: " + string.Join("; ", unlocked)); + "Restore with --locked-mode, or through the composite action's restore input: " + + string.Join("; ", unlocked)); } [Fact] @@ -100,7 +105,8 @@ public void Every_dotnet_job_uses_the_composite_setup_action() for (int index = 0; index < steps.Count; index++) { if (setupStep < 0 && - string.Equals(WorkflowFile.Scalar(steps[index], "uses"), WorkflowContract.SetupAction, StringComparison.Ordinal)) + string.Equals(WorkflowFile.Scalar(steps[index], "uses"), WorkflowContract.SetupAction, + StringComparison.Ordinal)) { setupStep = index; } @@ -162,12 +168,12 @@ private static bool RunsDotnet(string run) return false; } - [GeneratedRegex(@"(?m)(?:^|[\s;(|{&])dotnet\s", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"(?m)(?:^|[\s;(|{&])dotnet\s", RegexOptions.CultureInvariant, 1000)] private static partial Regex DotnetInvocation(); - [GeneratedRegex(@"\bdotnet\s+restore\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"\bdotnet\s+restore\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex DotnetRestore(); - [GeneratedRegex(@"(?:^|[\s(])\.?/?(?(?:eng|tests)/[\w./-]+\.ps1)\b", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"(?:^|[\s(])\.?/?(?(?:eng|tests)/[\w./-]+\.ps1)\b", RegexOptions.CultureInvariant, 1000)] private static partial Regex ScriptReference(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Supply.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Supply.cs index fba26c96..7b69487b 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Supply.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.Supply.cs @@ -15,7 +15,8 @@ public void Dependency_review_job_always_runs_and_reviews_only_pull_requests() // The gate requires success: the job itself never skips, only its steps choose by event (contract 1.11). Assert.Null(job.Condition); Assert.Empty(job.Needs()); - Assert.False(WorkflowFile.Has(job.Node, "permissions"), "dependency-review keeps the read-only top-level permissions."); + Assert.False(WorkflowFile.Has(job.Node, "permissions"), + "dependency-review keeps the read-only top-level permissions."); YamlMappingNode review = Assert.Single(job.StepsUsing("actions/dependency-review-action@")); Assert.Equal("github.event_name == 'pull_request'", WorkflowFile.Scalar(review, "if")); @@ -45,11 +46,13 @@ public void Dependency_review_configuration_blocks_advisories_and_unreviewed_lic Assert.Equal("moderate", WorkflowFile.Scalar(config, "fail-on-severity")); Assert.Equal(["runtime", "development"], - WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(config, "fail-on-scopes")))); + WorkflowFile.ScalarValues( + Assert.IsType(WorkflowFile.Sequence(config, "fail-on-scopes")))); Assert.Contains("MIT", WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(config, "allow-licenses"))), StringComparer.Ordinal); - foreach (string purl in WorkflowFile.ScalarValues(Assert.IsType(WorkflowFile.Sequence(config, "allow-dependencies-licenses")))) + foreach (string purl in WorkflowFile.ScalarValues( + Assert.IsType(WorkflowFile.Sequence(config, "allow-dependencies-licenses")))) { Assert.StartsWith("pkg:nuget/", purl, StringComparison.Ordinal); } @@ -57,7 +60,8 @@ public void Dependency_review_configuration_blocks_advisories_and_unreviewed_lic // The workflow sets these inline (contract 1.6); a second value here could silently disagree. foreach (string inline in new[] { "comment-summary-in-pr", "retry-on-snapshot-warnings", "config-file" }) { - Assert.False(WorkflowFile.Has(config, inline), $"{DependencyReviewConfig} must not repeat '{inline}', which ci.yml sets."); + Assert.False(WorkflowFile.Has(config, inline), + $"{DependencyReviewConfig} must not repeat '{inline}', which ci.yml sets."); } } @@ -76,6 +80,7 @@ public void Lock_file_job_restores_the_solution_and_every_out_of_solution_projec YamlMappingNode setup = Assert.Single(job.StepsUsing(WorkflowContract.SetupAction)); List targets = RestoreTargets(setup); Assert.Contains("CheatEngine.SDK.slnx", targets, StringComparer.Ordinal); - Assert.Contains("tests/CheatEngine.SDK.AotProbe/CheatEngine.SDK.AotProbe.csproj", targets, StringComparer.Ordinal); + Assert.Contains("tests/CheatEngine.SDK.AotProbe/CheatEngine.SDK.AotProbe.csproj", targets, + StringComparer.Ordinal); } } diff --git a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.cs b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.cs index 8f679c45..feaa13b9 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.cs +++ b/tests/CheatEngine.SDK.Repository.Tests/Workflows/WorkflowContractTests.cs @@ -92,6 +92,6 @@ private static string StripComments(string script) return string.Join('\n', lines); } - [GeneratedRegex(@"<#.*?#>", RegexOptions.Singleline | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex(@"<#.*?#>", RegexOptions.Singleline | RegexOptions.CultureInvariant, 1000)] private static partial Regex CommentBlock(); } diff --git a/tests/CheatEngine.SDK.Repository.Tests/packages.lock.json b/tests/CheatEngine.SDK.Repository.Tests/packages.lock.json index 105b4eff..737cc69e 100644 --- a/tests/CheatEngine.SDK.Repository.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Repository.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/DiagnosticsTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/DiagnosticsTests.cs index e989c604..be76f1d8 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/DiagnosticsTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/DiagnosticsTests.cs @@ -14,7 +14,8 @@ public sealed class DiagnosticsTests(RoslynFixture roslyn) : IClassFixture() : default;", generated, StringComparison.Ordinal); Assert.Contains("return __engineApiStatus;", generated, StringComparison.Ordinal); - Assert.Contains("return: LuaOperationStatus with out results; nil: absence;", generated, StringComparison.Ordinal); - Assert.Contains("global::CheatEngine.SDK.Lua.State.LuaState.MultipleResults", generated, StringComparison.Ordinal); + Assert.Contains("return: LuaOperationStatus with out results; nil: absence;", generated, + StringComparison.Ordinal); + Assert.Contains("global::CheatEngine.SDK.Lua.State.LuaState.MultipleResults", generated, + StringComparison.Ordinal); } [Fact] diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/EngineApiDiagnosticCatalogTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/EngineApiDiagnosticCatalogTests.cs index cb4eae1c..2883a65d 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/EngineApiDiagnosticCatalogTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Generator/EngineApiDiagnosticCatalogTests.cs @@ -32,7 +32,8 @@ public void Every_engine_api_descriptor_has_a_page_a_help_link_and_a_release_row Assert.Equal(DiagnosticSeverity.Error, descriptor.DefaultSeverity); Assert.Equal(HelpLinkBase + descriptor.Id + ".md", descriptor.HelpLinkUri); Assert.DoesNotMatch(@"\.$", title); - Assert.EndsWith(".", descriptor.Description.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); + Assert.EndsWith(".", descriptor.Description.ToString(CultureInfo.InvariantCulture), + StringComparison.Ordinal); string page = Path.Combine(root, "analyzers", "docs", descriptor.Id + ".md"); Assert.True(File.Exists(page), "Missing documentation page " + page + "."); @@ -48,7 +49,8 @@ private static DiagnosticDescriptor[] Descriptors() { return [ - .. typeof(EngineApiGenerator).Assembly.GetType("CheatEngine.SDK.SourceGenerators.EngineApi.EngineApiDiagnostics", + .. typeof(EngineApiGenerator).Assembly.GetType( + "CheatEngine.SDK.SourceGenerators.EngineApi.EngineApiDiagnostics", true)! .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) .Where(static field => field.FieldType == typeof(DiagnosticDescriptor)) @@ -59,7 +61,9 @@ .. typeof(EngineApiGenerator).Assembly.GetType("CheatEngine.SDK.SourceGenerators // The directory that holds CheatEngine.SDK.slnx, found from the test binaries upwards. private static string RepositoryRoot() { - for (DirectoryInfo? directory = new(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) + for (DirectoryInfo? directory = new(AppContext.BaseDirectory); + directory is not null; + directory = directory.Parent) { if (File.Exists(Path.Combine(directory.FullName, "CheatEngine.SDK.slnx"))) { @@ -67,6 +71,7 @@ private static string RepositoryRoot() } } - throw new InvalidOperationException("CheatEngine.SDK.slnx was not found above " + AppContext.BaseDirectory + "."); + throw new InvalidOperationException( + "CheatEngine.SDK.slnx was not found above " + AppContext.BaseDirectory + "."); } } diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Infrastructure/SpecSources.cs b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Infrastructure/SpecSources.cs index 904eece5..7a5d7538 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Infrastructure/SpecSources.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Infrastructure/SpecSources.cs @@ -10,12 +10,6 @@ internal static class SpecSources public const string Ce77 = "contract: ce77\nprovenance: ExactInstalledFile: CE 7.7 celua.txt test fixture\nminimum-ce: 7.7.0.10621\narchitecture: x64\nthread: unknown\nownership: none\n"; - /// A ce77 header block for and , blank line included. - public static string Ce77Header(string ns, string type) - { - return "namespace: " + ns + "\ntype: " + type + "\n" + Ce77 + "\n"; - } - /// /// The nominal case: two scalar reads (Try form) and two scalar writes (throwing form, boolean return), sharing /// no global. @@ -187,4 +181,10 @@ public static string Ce77Header(string ns, string type) nil: none doc: Calls a global with no arguments and no result. """; + + /// A ce77 header block for and , blank line included. + public static string Ce77Header(string ns, string type) + { + return "namespace: " + ns + "\ntype: " + type + "\n" + Ce77 + "\n"; + } } diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/ProductionSpecsTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/ProductionSpecsTests.cs index b3c1c3f4..be8cd508 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/ProductionSpecsTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/ProductionSpecsTests.cs @@ -32,7 +32,8 @@ public void Every_production_spec_parses_without_issues() SpecFileModel spec = SpecFileParser.Parse(fileName, text); Assert.True(spec.Issues.IsEmpty, - fileName + ": " + string.Join(" | ", spec.Issues.AsImmutableArray().Select(static i => i.Line + ": " + i.Message))); + fileName + ": " + string.Join(" | ", + spec.Issues.AsImmutableArray().Select(static i => i.Line + ": " + i.Message))); Assert.NotEqual(string.Empty, spec.TypeName, StringComparer.Ordinal); } } @@ -62,7 +63,8 @@ public void Every_production_spec_cites_the_pinned_celua_sha256() { SpecFileContract contract = Assert.IsType(SpecFileParser.Parse(fileName, text).Contract); - Assert.StartsWith("ExactInstalledFile: CE 7.7.0.10621 celua.txt", contract.Provenance, StringComparison.Ordinal); + Assert.StartsWith("ExactInstalledFile: CE 7.7.0.10621 celua.txt", contract.Provenance, + StringComparison.Ordinal); Assert.EndsWith("SHA-256 " + CeluaSha256, contract.Provenance, StringComparison.Ordinal); } } @@ -95,7 +97,8 @@ public void Unwired_specs_with_entries_are_reviewed_reservations() } Assert.True(ReviewedUnwiredSpecs.Contains(fileName), - fileName + " declares entries but is neither wired into CheatEngine.SDK.Engine nor a reviewed reservation."); + fileName + + " declares entries but is neither wired into CheatEngine.SDK.Engine nor a reviewed reservation."); } } @@ -107,7 +110,8 @@ public void Production_specs_never_claim_main_thread_affinity_without_evidence() SpecFileContract contract = Assert.IsType(SpecFileParser.Parse(fileName, text).Contract); Assert.True(contract.ThreadAffinity is "unknown" or "any", - fileName + " claims thread '" + contract.ThreadAffinity + "' without CE 7.7 evidence (audit SRC02-02)."); + fileName + " claims thread '" + contract.ThreadAffinity + + "' without CE 7.7 evidence (audit SRC02-02)."); } } } diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/SpecFileParserTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/SpecFileParserTests.cs index 37a52a68..ee6cb31a 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/SpecFileParserTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/Parsing/SpecFileParserTests.cs @@ -278,7 +278,8 @@ public void An_entry_missing_a_required_key_is_dropped(string missingKey) '\n', entry.Split('\n').Where(line => !line.StartsWith(missingKey + ":", StringComparison.Ordinal))); - SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", SpecSources.Ce77Header("Demo", "T") + edited); + SpecFileModel spec = + SpecFileParser.Parse("x.cheatengine-sdk-api.txt", SpecSources.Ce77Header("Demo", "T") + edited); Assert.Empty(spec.Calls.AsSpan().ToArray()); Assert.Contains(spec.Issues, issue => issue.Message.Contains("'" + missingKey + "'", StringComparison.Ordinal)); @@ -567,7 +568,8 @@ public void A_throwing_entry_without_a_return_is_a_void_wrapper() [InlineData("arg: 1bad:address")] public void A_malformed_or_unknown_kind_argument_drops_the_entry(string argLine) { - string text = "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + "\nglobal: readInteger\nmethod: M\nform: try\n" + argLine + + string text = "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + + "\nglobal: readInteger\nmethod: M\nform: try\n" + argLine + "\nresult: value:int32\nnil: none\ndoc: d.\n"; SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", text); @@ -582,7 +584,8 @@ public void A_malformed_or_unknown_kind_argument_drops_the_entry(string argLine) [InlineData("fixed: boolean:true; System.Console.WriteLine()")] public void A_fixed_argument_accepts_only_boolean_literals(string fixedLine) { - string text = "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + "\nglobal: readInteger\nmethod: M\nform: try\narg: address:address\n" + + string text = "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + + "\nglobal: readInteger\nmethod: M\nform: try\narg: address:address\n" + fixedLine + "\nresult: value:int32\nnil: none\ndoc: d.\n"; SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", text); @@ -864,7 +867,8 @@ public void Invalid_ce77_contract_facts_report_each_exact_value_location() public void An_invalid_argument_kind_records_its_value_column() { const string Text = - "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + "\n global: readInteger\n method: M\n form: try\n arg: address:notakind\n result: value:int32\n nil: none\n doc: d.\n"; + "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + + "\n global: readInteger\n method: M\n form: try\n arg: address:notakind\n result: value:int32\n nil: none\n doc: d.\n"; SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", Text); @@ -878,7 +882,8 @@ public void An_invalid_argument_kind_records_its_value_column() public void Parameter_and_generated_member_identity_collisions_drop_the_affected_entries() { const string Text = - "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + "\nglobal: readInteger\nmethod: BadParameter\nform: try\narg: __L:int32\nresult: value:int32\nnil: none\ndoc: bad.\n\nglobal: readInteger\nmethod: Read\nform: try\narg: address:address\nresult: value:int32\nnil: none\ndoc: raw core.\n\nglobal: readQword\nmethod: __ReadRaw\nform: try\nresult: value:int64\nnil: none\ndoc: collision.\n"; + "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + + "\nglobal: readInteger\nmethod: BadParameter\nform: try\narg: __L:int32\nresult: value:int32\nnil: none\ndoc: bad.\n\nglobal: readInteger\nmethod: Read\nform: try\narg: address:address\nresult: value:int32\nnil: none\ndoc: raw core.\n\nglobal: readQword\nmethod: __ReadRaw\nform: try\nresult: value:int64\nnil: none\ndoc: collision.\n"; SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", Text); @@ -893,7 +898,8 @@ public void Parameter_and_generated_member_identity_collisions_drop_the_affected public void A_parameter_named_operation_is_rejected_as_an_emitter_local_collision() { const string Text = - "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + "\nglobal: readInteger\nmethod: BadOperation\nform: try\narg: __operation:int32\nresult: value:int32\nnil: none\ndoc: bad.\n"; + "namespace: Demo\ntype: T\n" + SpecSources.Ce77 + + "\nglobal: readInteger\nmethod: BadOperation\nform: try\narg: __operation:int32\nresult: value:int32\nnil: none\ndoc: bad.\n"; SpecFileModel spec = SpecFileParser.Parse("x.cheatengine-sdk-api.txt", Text); @@ -919,7 +925,8 @@ public void Spec_with_entries_without_contract_reports_the_missing_ce77_contract Assert.Contains("'contract: ce77'", issue.Message, StringComparison.Ordinal); // A header-only reservation stays readable without the contract: it generates nothing either way. - SpecFileModel reservation = SpecFileParser.Parse("reservation.cheatengine-sdk-api.txt", "namespace: Demo\ntype: T\n"); + SpecFileModel reservation = + SpecFileParser.Parse("reservation.cheatengine-sdk-api.txt", "namespace: Demo\ntype: T\n"); Assert.True(reservation.Issues.IsEmpty); Assert.Null(reservation.Contract); } @@ -960,7 +967,8 @@ public void Argument_after_an_optional_argument_is_refused(string arguments, str } [Theory] - [InlineData("try", "opt-result: a:int32\nresult: b:int32\n", "'b:int32' is a required 'result' after an 'opt-result'")] + [InlineData("try", "opt-result: a:int32\nresult: b:int32\n", + "'b:int32' is a required 'result' after an 'opt-result'")] [InlineData("outcome", "rest: a:int64\nresult: b:int32\n", "'b:int32' must be declared before the 'rest' result")] [InlineData("outcome", "opt-result: a:string?\n", "'string?' cannot be an 'opt-result' kind")] [InlineData("outcome", "rest: a:address\n", "'address' cannot be a 'rest' kind")] @@ -1022,7 +1030,8 @@ public void Reformatting_a_spec_keeps_its_contract_and_call_model() string compact = SpecSources.Ce77Header("Demo", "T") + "global: g\nmethod: G\nform: outcome\narg: a:int32\nopt: b:address\nresult: r:int64\nopt-result: s:string\nnil: absence\ndoc: Reads.\n"; string reformatted = "# reformatted\r\n namespace: Demo \r\n\ttype: T\r\n" + - SpecSources.Ce77.Replace("\n", "\r\n # a comment inside the header\r\n", StringComparison.Ordinal) + + SpecSources.Ce77.Replace("\n", "\r\n # a comment inside the header\r\n", + StringComparison.Ordinal) + "\r\n\r\n # a comment before the entry\r\n global: g\r\n method: G\r\n # a comment inside the entry\r\n" + " form:outcome\r\n arg: a:int32\r\n opt: b:address \r\n result: r:int64\r\n opt-result: s:string\r\n" + " nil: absence\r\n doc: Reads.\r\n\r\n"; diff --git a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/packages.lock.json b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/packages.lock.json index 4902922c..51588b64 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.SourceGenerators.EngineApi.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NameEscapingTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NameEscapingTests.cs index 2803321c..e4cb4d9d 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NameEscapingTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NameEscapingTests.cs @@ -61,7 +61,7 @@ public void Generator_display_name_is_escaped_into_a_u8_literal(string caseKey) // Holds because the plugin type of these cases (Demo.DemoPlugin) is ASCII: whatever the display name is, the // literal adds no other character. Identifiers are written as declared (PluginLocationTests, non-ASCII case). Assert.All(generated, - static c => Assert.True(c is '\n' or >= ' ' and <= '~', + static c => Assert.True(c is '\n' or (>= ' ' and <= '~'), $"Non-ASCII character U+{(int) c:X4} in generated text.")); run.AssertCompilesClean(); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NominalOutputTests.cs b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NominalOutputTests.cs index e4c12e78..78c21747 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NominalOutputTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/Generator/NominalOutputTests.cs @@ -105,8 +105,11 @@ public void Generator_bootstrap_constructs_the_plugin_without_reflection() GeneratorRun run = roslyn.Run(PluginSources.Nominal); string text = run.SingleGeneratedText; - foreach (string reflective in (string[]) ["Activator", "CreateInstance", "GetTypes", "GetType(", "typeof(", - "System.Reflection", "Assembly."]) + foreach (string reflective in (string[]) + [ + "Activator", "CreateInstance", "GetTypes", "GetType(", "typeof(", + "System.Reflection", "Assembly." + ]) { Assert.DoesNotContain(reflective, text, StringComparison.Ordinal); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/packages.lock.json b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/packages.lock.json index 482ceb2a..2643f431 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.SourceGenerators.EntryPoint.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalOptionalArgumentEndToEndTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalOptionalArgumentEndToEndTests.cs index d127e7b0..872f31b9 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalOptionalArgumentEndToEndTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalOptionalArgumentEndToEndTests.cs @@ -133,8 +133,11 @@ public void Warm_optional_argument_form_allocates_nothing() AllocationGate.AssertZero(() => { if (!tryArity(1, LuaOptional.Of(2L), out long present) || present != 2 - || !tryArity(1, default, out long omitted) || omitted != 1 - || !detailed(1, LuaOptional.Nil(), LuaOptional.Of(true), out long nil).IsSuccess || nil != 3) + || !tryArity(1, default, out long omitted) || + omitted != 1 + || !detailed(1, LuaOptional.Nil(), + LuaOptional.Of(true), out long nil).IsSuccess || + nil != 3) { throw new InvalidOperationException("wrong count"); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalQ22MatrixEndToEndTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalQ22MatrixEndToEndTests.cs index 0110fc75..c0da154c 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalQ22MatrixEndToEndTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalQ22MatrixEndToEndTests.cs @@ -296,7 +296,8 @@ public void Custom_marshaller_exception_after_a_successful_call_propagates_with_ EvenDelegate even = assembly.Delegate(Q22Type, "Even"); // A marshaller's own exception is not a Lua failure: no form swallows or reclassifies it. - Assert.Equal("negative token", Assert.Throws(() => tryEven("negative", out _)).Message); + Assert.Equal("negative token", + Assert.Throws(() => tryEven("negative", out _)).Message); Assert.Equal(0, L.Top); Assert.Throws(() => outcome("negative", out _)); Assert.Equal(0, L.Top); diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalResultCountEndToEndTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalResultCountEndToEndTests.cs index 09f6ebef..0550814d 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalResultCountEndToEndTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/EndToEnd/LuaGlobalResultCountEndToEndTests.cs @@ -59,7 +59,8 @@ public void Missing_required_result_before_an_optional_one_is_missing_not_nil() Assert.Equal(LuaOperationStatusKind.MissingResult, detailed(0, out long missing, out _).Kind); Assert.Equal(0, missing); Assert.Equal(LuaOperationStatusKind.NilResult, detailed(1, out _, out _).Kind); - Assert.Equal(LuaOperationStatusKind.Success, detailed(2, out long onlyFirst, out LuaOptional absent).Kind); + Assert.Equal(LuaOperationStatusKind.Success, + detailed(2, out long onlyFirst, out LuaOptional absent).Kind); Assert.Equal(7, onlyFirst); Assert.True(absent.IsOmitted); Assert.Equal(LuaOperationStatusKind.Success, detailed(3, out long first, out LuaOptional second).Kind); @@ -67,7 +68,8 @@ public void Missing_required_result_before_an_optional_one_is_missing_not_nil() Assert.Equal(LuaOptional.Of(8L), second); Assert.Equal(LuaOperationStatusKind.Success, detailed(8, out _, out LuaOptional explicitNil).Kind); Assert.True(explicitNil.IsNil); - Assert.Equal(LuaOperationStatusKind.InvalidResult, detailed(7, out long defaulted, out LuaOptional bad).Kind); + Assert.Equal(LuaOperationStatusKind.InvalidResult, + detailed(7, out long defaulted, out LuaOptional bad).Kind); Assert.Equal(0, defaulted); Assert.True(bad.IsOmitted); @@ -129,7 +131,8 @@ public void Variadic_result_copies_every_value_and_reports_the_count() LuaTest.Run(L, OptionalBindingSources.StandIns); GeneratedAssembly assembly = LoadSuite(roslyn); SequenceDelegate sequence = assembly.Delegate(OptionalsType, "Sequence"); - SequenceAfterOneDelegate afterOne = assembly.Delegate(OptionalsType, "SequenceAfterOne"); + SequenceAfterOneDelegate afterOne = + assembly.Delegate(OptionalsType, "SequenceAfterOne"); Span values = stackalloc long[8]; Assert.Equal(LuaOperationStatusKind.Success, sequence(3, default, values, out int count).Kind); @@ -165,7 +168,8 @@ public void Variadic_result_above_capacity_is_refused_with_the_needed_count_and_ LuaTest.Run(L, OptionalBindingSources.StandIns); GeneratedAssembly assembly = LoadSuite(roslyn); SequenceDelegate sequence = assembly.Delegate(OptionalsType, "Sequence"); - SequenceAfterOneDelegate afterOne = assembly.Delegate(OptionalsType, "SequenceAfterOne"); + SequenceAfterOneDelegate afterOne = + assembly.Delegate(OptionalsType, "SequenceAfterOne"); Span small = stackalloc long[2]; Assert.Equal(LuaOperationStatusKind.ResultCapacityExceeded, sequence(5, default, small, out int needed).Kind); @@ -279,6 +283,7 @@ private static GeneratedAssembly LoadSuite(RoslynFixture roslyn) private delegate LuaOperationStatus SequenceDelegate(int count, LuaOptional bad, Span values, out int valueCount); - private delegate LuaOperationStatus SequenceAfterOneDelegate(int count, out long first, out LuaOptional second, + private delegate LuaOperationStatus SequenceAfterOneDelegate(int count, out long first, + out LuaOptional second, Span rest, out int restCount); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/IncrementalityTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/IncrementalityTests.cs index 38f3dd4b..b3550523 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/IncrementalityTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/IncrementalityTests.cs @@ -352,7 +352,8 @@ public void Pipeline_step_values_hold_no_roslyn_objects() private static string[] OptionalMembers() { string body = OptionalBindingSources.GlobalSuite.ReplaceLineEndings("\n"); - int open = body.IndexOf("{\n", body.IndexOf("class Optionals", StringComparison.Ordinal), StringComparison.Ordinal); + int open = body.IndexOf("{\n", body.IndexOf("class Optionals", StringComparison.Ordinal), + StringComparison.Ordinal); int close = body.LastIndexOf('}'); return [ diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaGlobalOutputTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaGlobalOutputTests.cs index 94b32321..f16ce313 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaGlobalOutputTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/LuaGlobalOutputTests.cs @@ -337,7 +337,8 @@ public void Generator_variadic_pair_is_emitted_only_for_the_outcome_form() Assert.Contains("return global::CheatEngine.SDK.Lua.CompilerServices.LuaCallSupport.Fail(__L, __top, __rest);", body, StringComparison.Ordinal); - const string TryForm = "using System;\nusing CheatEngine.SDK.Annotations.Lua;\nnamespace Demo; public static partial class Holder { [LuaGlobal(\"seq\")] public static partial bool TrySeq(int n, Span values, out int count); }"; + const string TryForm = + "using System;\nusing CheatEngine.SDK.Annotations.Lua;\nnamespace Demo; public static partial class Holder { [LuaGlobal(\"seq\")] public static partial bool TrySeq(int n, Span values, out int count); }"; roslyn.Run(TryForm).AssertNoOutput(); } @@ -351,9 +352,11 @@ public void Generator_outcome_form_with_several_results_defaults_every_other_res run.AssertCompilesClean(); string body = Section(run.SingleGeneratedText, "Divide(long a, long b", "\n }\n"); - Assert.Contains("remainder = default!;\n return global::CheatEngine.SDK.Lua.CompilerServices.LuaCallSupport.Fail(__L, __top, __L.IsNil(-2)", + Assert.Contains( + "remainder = default!;\n return global::CheatEngine.SDK.Lua.CompilerServices.LuaCallSupport.Fail(__L, __top, __L.IsNil(-2)", body, StringComparison.Ordinal); - Assert.Contains("quotient = default;\n return global::CheatEngine.SDK.Lua.CompilerServices.LuaCallSupport.Fail(__L, __top, __L.IsNil(-1)", + Assert.Contains( + "quotient = default;\n return global::CheatEngine.SDK.Lua.CompilerServices.LuaCallSupport.Fail(__L, __top, __L.IsNil(-1)", body, StringComparison.Ordinal); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/NoOutputTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/NoOutputTests.cs index cd59d095..7d4ab3c8 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/NoOutputTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Generator/NoOutputTests.cs @@ -385,13 +385,16 @@ public static TheoryData InvalidGlobals const string Status = "CheatEngine.SDK.Lua.Calls.LuaOperationStatus"; yield return ( "required argument after an optional one", - "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + " a, int b); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + + " a, int b); }"); yield return ( "required result after an optional one", - "public static partial class T { [LuaGlobal(\"g\")] public static partial bool TryG(out " + Optional + " a, out int b); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial bool TryG(out " + Optional + + " a, out int b); }"); yield return ( "copy-out result after an optional one", - "public static partial class T { [LuaGlobal(\"g\")] public static partial bool TryG(out " + Optional + " a, Span d, out int w); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial bool TryG(out " + Optional + + " a, Span d, out int w); }"); yield return ( "variadic pair in the Try form", "public static partial class T { [LuaGlobal(\"g\")] public static partial bool TryG(Span v, out int c); }"); @@ -400,31 +403,40 @@ public static TheoryData InvalidGlobals "public static partial class T { [LuaGlobal(\"g\")] public static partial void G(Span v, out int c); }"); yield return ( "result after the variadic pair", - "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + " G(Span v, out int c, out int d); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + + " G(Span v, out int c, out int d); }"); yield return ( "two variadic pairs", - "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + " G(Span v, out int c, Span w, out int e); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + + " G(Span v, out int c, Span w, out int e); }"); yield return ( "variadic string element", - "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + " G(Span v, out int c); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + + " G(Span v, out int c); }"); yield return ( "variadic span without a count", - "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + " G(Span v); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Status + + " G(Span v); }"); yield return ( "optional nullable string", - "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + " a); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + + " a); }"); yield return ( "optional object", - "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + " a); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + + " a); }"); yield return ( "nested optional", - "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + "<" + Optional + "> a); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + "<" + + Optional + "> a); }"); yield return ( "optional throwing return", - "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Optional + " G(int a); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial " + Optional + + " G(int a); }"); yield return ( "optional with a C# default value", - "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + " a = default); }"); + "public static partial class T { [LuaGlobal(\"g\")] public static partial int G(" + Optional + + " a = default); }"); } // CESDK2010 and CESDK2013 shapes of an exported function. @@ -433,13 +445,16 @@ public static TheoryData InvalidGlobals const string Optional = "CheatEngine.SDK.Lua.Marshalling.LuaOptional"; yield return ( "required parameter after an optional one", - "public static partial class T { [LuaFunction(\"f\")] public static int F(" + Optional + " a, int b) => b; }"); + "public static partial class T { [LuaFunction(\"f\")] public static int F(" + Optional + + " a, int b) => b; }"); yield return ( "optional return", - "public static partial class T { [LuaFunction(\"f\")] public static " + Optional + " F(int a) => default; }"); + "public static partial class T { [LuaFunction(\"f\")] public static " + Optional + + " F(int a) => default; }"); yield return ( "optional nullable string parameter", - "public static partial class T { [LuaFunction(\"f\")] public static int F(" + Optional + " a) => 0; }"); + "public static partial class T { [LuaFunction(\"f\")] public static int F(" + Optional + + " a) => 0; }"); } [Fact] diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/FidelityBindingSources.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/FidelityBindingSources.cs index a36ffa8a..7fa9d116 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/FidelityBindingSources.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/FidelityBindingSources.cs @@ -8,30 +8,6 @@ namespace CheatEngine.SDK.SourceGenerators.LuaBindings.Tests.Infrastructure; /// internal static class FidelityBindingSources { - /// - /// q22(kind) returns one shape per kind: nil, false, true, 0, 42, 7 (odd), -2 - /// (negative), '', {}, no value at all, a string error and a table error whose __tostring - /// raises. - /// - public static ReadOnlySpan Q22StandIns => """ - local raising = setmetatable({}, { __tostring = function() error('tostring boom') end }) - local cases = { - ['nil'] = function() return nil end, - ['false'] = function() return false end, - ['true'] = function() return true end, - zero = function() return 0 end, - value = function() return 42 end, - odd = function() return 7 end, - negative = function() return -2 end, - empty = function() return '' end, - table = function() return {} end, - none = function() end, - raise = function() error('boom') end, - raise_table = function() error(raising) end, - } - function q22(kind) return cases[kind]() end - """u8; - /// Every Q22 form: Try, Outcome and Throwing over integer, boolean, string, optional and custom results. public const string Q22Suite = """ using System; @@ -110,11 +86,6 @@ public static partial class Q22 } """; - /// boundary(expression) evaluates a Lua expression, so each row is a value Lua itself produced. - public static ReadOnlySpan NumericStandIns => """ - function boundary(expression) return assert(load('return ' .. expression))() end - """u8; - /// Integer, 64-bit and address results in every form, and the same kinds as exported function arguments. public const string NumericSuite = """ using CheatEngine.SDK.Annotations.Lua; @@ -162,18 +133,6 @@ public static partial class Numbers } """; - /// - /// s_value(key) returns a string with embedded NULs, multibyte UTF-8, invalid UTF-8 or the empty string; - /// s_len returns the byte length Lua received, s_echo the string itself, s_type its type. - /// - public static ReadOnlySpan StringStandIns => """ - local values = { nul = 'a\0b\0', multibyte = '\u{E9}\u{6F22}\u{1F600}', invalid = '\255\254A', empty = '' } - function s_value(key) return values[key] end - function s_len(s) return #s end - function s_echo(s) return s end - function s_type(s) return type(s) end - """u8; - /// String and UTF-8 arguments and results in every shape, and a nullable string function argument. public const string StringSuite = """ using System; @@ -212,4 +171,45 @@ public static partial class Strings public static string Describe(string? text) => text is null ? "null" : "text:" + text.Length; } """; + + /// + /// q22(kind) returns one shape per kind: nil, false, true, 0, 42, 7 (odd), -2 + /// (negative), '', {}, no value at all, a string error and a table error whose __tostring + /// raises. + /// + public static ReadOnlySpan Q22StandIns => """ + local raising = setmetatable({}, { __tostring = function() error('tostring boom') end }) + local cases = { + ['nil'] = function() return nil end, + ['false'] = function() return false end, + ['true'] = function() return true end, + zero = function() return 0 end, + value = function() return 42 end, + odd = function() return 7 end, + negative = function() return -2 end, + empty = function() return '' end, + table = function() return {} end, + none = function() end, + raise = function() error('boom') end, + raise_table = function() error(raising) end, + } + function q22(kind) return cases[kind]() end + """u8; + + /// boundary(expression) evaluates a Lua expression, so each row is a value Lua itself produced. + public static ReadOnlySpan NumericStandIns => """ + function boundary(expression) return assert(load('return ' .. expression))() end + """u8; + + /// + /// s_value(key) returns a string with embedded NULs, multibyte UTF-8, invalid UTF-8 or the empty string; + /// s_len returns the byte length Lua received, s_echo the string itself, s_type its type. + /// + public static ReadOnlySpan StringStandIns => """ + local values = { nul = 'a\0b\0', multibyte = '\u{E9}\u{6F22}\u{1F600}', invalid = '\255\254A', empty = '' } + function s_value(key) return values[key] end + function s_len(s) return #s end + function s_echo(s) return s end + function s_type(s) return type(s) end + """u8; } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/OptionalBindingSources.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/OptionalBindingSources.cs index 77977459..18f8d6b6 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/OptionalBindingSources.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Infrastructure/OptionalBindingSources.cs @@ -7,47 +7,6 @@ namespace CheatEngine.SDK.SourceGenerators.LuaBindings.Tests.Infrastructure; /// internal static class OptionalBindingSources { - /// The stand-in globals every optional suite runs against. - public static ReadOnlySpan StandIns => """ - calls = 0 - function arity(...) calls = calls + 1 return select('#', ...) end - function kinds(...) - local t = table.pack(...) - local out = {} - for i = 1, t.n do out[#out + 1] = type(t[i]) end - return table.concat(out, ',') - end - function readBytesLike(...) - local n = select('#', ...) - local address, count, asTable = ... - if n == 1 then return 'one byte' end - if n == 2 then return 'values' end - if asTable == nil then return 'nil flag' end - if asTable then return 'table' end - return 'values' - end - function shape(mode) - if mode == 0 then return end - if mode == 1 then return nil end - if mode == 2 then return 7 end - if mode == 3 then return 7, 8 end - if mode == 4 then return true, 'warn' end - if mode == 5 then return 1970354901756285, {} end - if mode == 6 then return 'text' end - if mode == 7 then return 7, 'x' end - if mode == 8 then return 7, nil end - error('boom') - end - function seq(n, bad) - if bad == 1 then return 1, nil, 3 end - if bad == 2 then return 1, 'x', 3 end - if n < 0 then error('negative') end - local t = {} - for i = 1, n do t[i] = i * 10 end - return table.unpack(t, 1, n) - end - """u8; - /// Every supported optional argument, optional result and variadic shape of a bound global. public const string GlobalSuite = """ using System; @@ -123,4 +82,45 @@ private static string State(LuaOptional value) value.IsOmitted ? "omitted" : value.IsNil ? "nil" : value.Value.ToString()!; } """; + + /// The stand-in globals every optional suite runs against. + public static ReadOnlySpan StandIns => """ + calls = 0 + function arity(...) calls = calls + 1 return select('#', ...) end + function kinds(...) + local t = table.pack(...) + local out = {} + for i = 1, t.n do out[#out + 1] = type(t[i]) end + return table.concat(out, ',') + end + function readBytesLike(...) + local n = select('#', ...) + local address, count, asTable = ... + if n == 1 then return 'one byte' end + if n == 2 then return 'values' end + if asTable == nil then return 'nil flag' end + if asTable then return 'table' end + return 'values' + end + function shape(mode) + if mode == 0 then return end + if mode == 1 then return nil end + if mode == 2 then return 7 end + if mode == 3 then return 7, 8 end + if mode == 4 then return true, 'warn' end + if mode == 5 then return 1970354901756285, {} end + if mode == 6 then return 'text' end + if mode == 7 then return 7, 'x' end + if mode == 8 then return 7, nil end + error('boom') + end + function seq(n, bad) + if bad == 1 then return 1, nil, 3 end + if bad == 2 then return 1, 'x', 3 end + if n < 0 then error('negative') end + local t = {} + for i = 1, n do t[i] = i * 10 end + return table.unpack(t, 1, n) + end + """u8; } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Model/LuaGlobalTablesTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Model/LuaGlobalTablesTests.cs index a4ecba71..c32ad926 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Model/LuaGlobalTablesTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/Model/LuaGlobalTablesTests.cs @@ -95,10 +95,11 @@ public void Optional_argument_model_is_value_equal_across_runs() new LuaArgumentModel("count", LuaValueKind.Int32, false) ]) }); - Assert.NotEqual(first, first with - { - Results = new EquatableArray([LuaResultModel.Value(LuaValueKind.Int64, "value")]) - }); + Assert.NotEqual(first, + first with + { + Results = new EquatableArray([LuaResultModel.Value(LuaValueKind.Int64, "value")]) + }); Assert.Equal( new LuaArgumentModel("count", LuaValueKind.Int32, false, false, null, null), new LuaArgumentModel("count", LuaValueKind.Int32, false)); @@ -107,7 +108,8 @@ public void Optional_argument_model_is_value_equal_across_runs() private static LuaGlobalCallModel OptionalCall() { - return new LuaGlobalCallModel("readBytes", LuaGlobalCallModel.CacheFieldFor("readBytes"), "public static partial", + return new LuaGlobalCallModel("readBytes", LuaGlobalCallModel.CacheFieldFor("readBytes"), + "public static partial", "ReadBytes", string.Empty, new EquatableArray([ new LuaArgumentModel("address", LuaValueKind.Address, false), diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/SharedCode/LuaGlobalCallEmitterTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/SharedCode/LuaGlobalCallEmitterTests.cs index 9cbb8d14..6220f3d2 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/SharedCode/LuaGlobalCallEmitterTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/SharedCode/LuaGlobalCallEmitterTests.cs @@ -289,8 +289,11 @@ public void Optional_and_variadic_shapes_are_reported_by_the_model_and_their_loc "(global::CheatEngine.SDK.Lua.Marshalling.LuaOptional n, out long first, out global::CheatEngine.SDK.Lua.Marshalling.LuaOptional second, scoped global::System.Span values, out int count)", writer.ToString()); - foreach (string local in (string[]) ["__L", "__operation", "__top", "__ok", "__status", "__result", - "__resolution", "__exception", "__argc", "__rest"]) + foreach (string local in (string[]) + [ + "__L", "__operation", "__top", "__ok", "__status", "__result", + "__resolution", "__exception", "__argc", "__rest" + ]) { Assert.True(LuaGlobalCallEmitter.IsReservedLocal(local), local); } diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/packages.lock.json b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/packages.lock.json index ffda23d3..c599bc14 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBindings.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogDiagnosticsTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogDiagnosticsTests.cs index 2ad33941..a0db3bef 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogDiagnosticsTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogDiagnosticsTests.cs @@ -34,7 +34,8 @@ public void Duplicate_opcode_reports_both_external_file_entries_and_emits_nothin string text = CatalogSources.ReverseOpcodeOrder .Replace("\"opcode\": 10", "\"opcode\": 0", StringComparison.Ordinal) .Replace("0x0000000000000401", "0x0000000000000001", StringComparison.Ordinal); - GeneratorRun run = RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", text); + GeneratorRun run = + RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", text); Assert.Empty(run.GeneratedSources); Assert.Equal(2, run.GeneratorDiagnostics.Length); diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogEmissionTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogEmissionTests.cs index 0ced0481..e21b69a1 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogEmissionTests.cs +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/CatalogEmissionTests.cs @@ -12,7 +12,8 @@ public sealed class CatalogEmissionTests public void Catalog_operations_emit_a_numeric_sorted_enum_and_required_bitmap() { GeneratorRun run = - RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", CatalogSources.ReverseOpcodeOrder); + RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", + CatalogSources.ReverseOpcodeOrder); run.AssertCompilesClean(); string generated = run.SingleGeneratedText; @@ -30,7 +31,8 @@ public void Catalog_operations_emit_a_numeric_sorted_enum_and_required_bitmap() [Fact] public void Production_catalog_emits_all_C11_operations_and_the_exact_required_bitmap() { - GeneratorRun run = RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", ProductionCatalog.Read()); + GeneratorRun run = RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", + ProductionCatalog.Read()); run.AssertCompilesClean(); string generated = run.SingleGeneratedText; @@ -62,7 +64,8 @@ public void Production_catalog_emits_all_C11_operations_and_the_exact_required_b [Fact] public void Catalog_an_identical_rerun_is_deterministic() { - InMemoryAdditionalText additionalText = new("libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", + InMemoryAdditionalText additionalText = new( + "libs/CheatEngine.SDK.Lua.Interop/Protected/protected-operations.json", CatalogSources.ReverseOpcodeOrder); CSharpCompilation compilation = RoslynFixture.CreateCompilation(); GeneratorRun first = GeneratorRun.Execute(RoslynFixture.CreateDriver(additionalText), compilation); @@ -122,7 +125,8 @@ public void Replacing_a_valid_catalogue_with_an_invalid_one_removes_generated_so [Fact] public void An_unrelated_additional_file_is_silent() { - GeneratorRun run = RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/notes.json", CatalogSources.ReverseOpcodeOrder); + GeneratorRun run = RoslynFixture.Run("libs/CheatEngine.SDK.Lua.Interop/Protected/notes.json", + CatalogSources.ReverseOpcodeOrder); Assert.Empty(run.GeneratedSources); Assert.Empty(run.GeneratorDiagnostics); diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/packages.lock.json b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/packages.lock.json index 6d46d526..1e2f345b 100644 --- a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Tests.Shared/NativeLua/PartialLuaModule.cs b/tests/CheatEngine.SDK.Tests.Shared/NativeLua/PartialLuaModule.cs index 1541a551..d4b7055f 100644 --- a/tests/CheatEngine.SDK.Tests.Shared/NativeLua/PartialLuaModule.cs +++ b/tests/CheatEngine.SDK.Tests.Shared/NativeLua/PartialLuaModule.cs @@ -46,20 +46,6 @@ public nint Handle private set; } - /// Copies , removes from the copy and loads it. - /// The image is not PE32+ or does not export . - public static PartialLuaModule Load(string fixturePath) - { - byte[] image = File.ReadAllBytes(fixturePath); - RenameExport(image, RemovedExport, ReplacementName); - string directory = Path.Combine(Path.GetTempPath(), "CheatEngine.SDK.Tests", - "partial-lua-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(directory); - string path = Path.Combine(directory, "lua53-partial.dll"); - File.WriteAllBytes(path, image); - return new PartialLuaModule(directory, NativeLibrary.Load(path)); - } - /// Frees the copy and deletes its folder. public void Dispose() { @@ -83,6 +69,20 @@ public void Dispose() } } + /// Copies , removes from the copy and loads it. + /// The image is not PE32+ or does not export . + public static PartialLuaModule Load(string fixturePath) + { + byte[] image = File.ReadAllBytes(fixturePath); + RenameExport(image, RemovedExport, ReplacementName); + string directory = Path.Combine(Path.GetTempPath(), "CheatEngine.SDK.Tests", + "partial-lua-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + string path = Path.Combine(directory, "lua53-partial.dll"); + File.WriteAllBytes(path, image); + return new PartialLuaModule(directory, NativeLibrary.Load(path)); + } + /// /// Overwrites the export name of a PE32+ in place with /// , found through the export directory rather than by searching the bytes. @@ -94,7 +94,8 @@ internal static void RenameExport(byte[] image, string existing, string replacem { if (replacement.Length != existing.Length) { - throw new ArgumentException("The replacement must have the length of the original name.", nameof(replacement)); + throw new ArgumentException("The replacement must have the length of the original name.", + nameof(replacement)); } int signature = BinaryPrimitives.ReadInt32LittleEndian(image.AsSpan(PeHeaderPointerOffset)); @@ -107,7 +108,9 @@ internal static void RenameExport(byte[] image, string existing, string replacem int sectionCount = BinaryPrimitives.ReadUInt16LittleEndian(image.AsSpan(signature + 6)); int sections = optionalHeader + BinaryPrimitives.ReadUInt16LittleEndian(image.AsSpan(signature + 20)); - uint exportRva = BinaryPrimitives.ReadUInt32LittleEndian(image.AsSpan(optionalHeader + ExportDirectoryOffsetInOptionalHeader)); + uint exportRva = + BinaryPrimitives.ReadUInt32LittleEndian( + image.AsSpan(optionalHeader + ExportDirectoryOffsetInOptionalHeader)); int exportDirectory = ToFileOffset(image, sections, sectionCount, exportRva); int nameCount = BinaryPrimitives.ReadInt32LittleEndian(image.AsSpan(exportDirectory + 24)); int nameTable = ToFileOffset(image, sections, sectionCount, @@ -129,7 +132,8 @@ internal static void RenameExport(byte[] image, string existing, string replacem } bool staysSorted = (target == 0 || string.CompareOrdinal(names[target - 1].Name, replacement) < 0) && - (target == names.Count - 1 || string.CompareOrdinal(replacement, names[target + 1].Name) < 0); + (target == names.Count - 1 || + string.CompareOrdinal(replacement, names[target + 1].Name) < 0); if (!staysSorted) { throw new InvalidOperationException($"'{replacement}' would leave the export name table unsorted."); diff --git a/tests/CheatEngine.SDK.Tests.Shared/packages.lock.json b/tests/CheatEngine.SDK.Tests.Shared/packages.lock.json index dabc335c..0fcd840b 100644 --- a/tests/CheatEngine.SDK.Tests.Shared/packages.lock.json +++ b/tests/CheatEngine.SDK.Tests.Shared/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "MinVer": { "type": "Direct", diff --git a/tests/CheatEngine.SDK.Tests/Build/NativeAotProfileTargetTests.cs b/tests/CheatEngine.SDK.Tests/Build/NativeAotProfileTargetTests.cs index 4cecb7ff..f9cd2616 100644 --- a/tests/CheatEngine.SDK.Tests/Build/NativeAotProfileTargetTests.cs +++ b/tests/CheatEngine.SDK.Tests/Build/NativeAotProfileTargetTests.cs @@ -5,7 +5,8 @@ namespace CheatEngine.SDK.Tests.Build; /// -/// The packaged build target CheatEngineSdkWarnNativeAotPluginProfile (src/CheatEngine.SDK/build/CheatEngine.SDK.targets) +/// The packaged build target CheatEngineSdkWarnNativeAotPluginProfile ( +/// src/CheatEngine.SDK/build/CheatEngine.SDK.targets) /// warns with CESDK9102 when a direct consumer library sets PublishAot=true: a NativeAOT plugin /// DLL cannot be unloaded by Cheat Engine's FreeLibrary (audit F02, EXT-01), so the only supported plugin /// profile is the managed hostfxr route. An executable publishing with NativeAOT is not warned. @@ -30,7 +31,7 @@ public sealed class NativeAotProfileTargetTests [Trait("Qualification", "Q41")] public async Task PublishAot_library_consumer_gets_CESDK9102(string nativeLib) { - ProcessResult result = await RunTargetAsync("Library", publishAot: "true", nativeLib, noWarn: false); + ProcessResult result = await RunTargetAsync("Library", "true", nativeLib, false); Assert.True(result.ExitCode == 0, result.CombinedOutput); Assert.Contains("warning CESDK9102", result.StandardOutput, StringComparison.Ordinal); @@ -41,7 +42,8 @@ public async Task PublishAot_library_consumer_gets_CESDK9102(string nativeLib) [Fact] public void CESDK9102_warning_links_its_rule_page_and_runs_before_the_build() { - XDocument targets = XDocument.Load(RepositoryLayout.PathOf("src/CheatEngine.SDK/build/CheatEngine.SDK.targets")); + XDocument targets = + XDocument.Load(RepositoryLayout.PathOf("src/CheatEngine.SDK/build/CheatEngine.SDK.targets")); XElement target = Assert.Single(targets.Root!.Elements("Target"), static element => string.Equals((string?) element.Attribute("Name"), TargetName, StringComparison.Ordinal)); @@ -59,7 +61,7 @@ public void CESDK9102_warning_links_its_rule_page_and_runs_before_the_build() [Trait("Qualification", "Q41")] public async Task PublishAot_executable_consumer_gets_no_CESDK9102(string outputType) { - ProcessResult result = await RunTargetAsync(outputType, publishAot: "true", nativeLib: "", noWarn: false); + ProcessResult result = await RunTargetAsync(outputType, "true", "", false); Assert.True(result.ExitCode == 0, result.CombinedOutput); Assert.DoesNotContain("CESDK9102", result.CombinedOutput, StringComparison.Ordinal); @@ -71,7 +73,7 @@ public async Task PublishAot_executable_consumer_gets_no_CESDK9102(string output [Trait("Qualification", "Q41")] public async Task Consumer_without_PublishAot_gets_no_CESDK9102(string publishAot) { - ProcessResult result = await RunTargetAsync("Library", publishAot, nativeLib: "Shared", noWarn: false); + ProcessResult result = await RunTargetAsync("Library", publishAot, "Shared", false); Assert.True(result.ExitCode == 0, result.CombinedOutput); Assert.DoesNotContain("CESDK9102", result.CombinedOutput, StringComparison.Ordinal); @@ -81,7 +83,7 @@ public async Task Consumer_without_PublishAot_gets_no_CESDK9102(string publishAo [Trait("Qualification", "Q41")] public async Task NoWarn_demotes_CESDK9102_to_a_message() { - ProcessResult result = await RunTargetAsync("Library", publishAot: "true", nativeLib: "Shared", noWarn: true); + ProcessResult result = await RunTargetAsync("Library", "true", "Shared", true); // Detailed verbosity: MSBuild logs a warning demoted by NoWarn as a low-importance message. Assert.True(result.ExitCode == 0, result.CombinedOutput); @@ -101,18 +103,18 @@ private static async Task RunTargetAsync(string outputType, strin string project = Path.Combine(directory, "NativeAotProfileConsumer.csproj"); File.Copy(RepositoryLayout.PathOf("global.json"), Path.Combine(directory, "global.json")); await File.WriteAllTextAsync(project, $""" - - - - net10.0 - {outputType} - {publishAot} - {nativeLib} - $(NoWarn);CESDK9102 - - - - """, TestContext.Current.CancellationToken); + + + + net10.0 + {outputType} + {publishAot} + {nativeLib} + $(NoWarn);CESDK9102 + + + + """, TestContext.Current.CancellationToken); string arguments = string.Create(CultureInfo.InvariantCulture, $"msbuild \"{project}\" -t:{TargetName} -nologo -v:{(noWarn ? "d" : "n")} -p:ImportDirectoryBuildProps=false -p:ImportDirectoryBuildTargets=false -p:ImportDirectoryPackagesProps=false"); diff --git a/tests/CheatEngine.SDK.Tests/Infrastructure/CommittedFile.cs b/tests/CheatEngine.SDK.Tests/Infrastructure/CommittedFile.cs index ab9744bb..61b8dbc6 100644 --- a/tests/CheatEngine.SDK.Tests/Infrastructure/CommittedFile.cs +++ b/tests/CheatEngine.SDK.Tests/Infrastructure/CommittedFile.cs @@ -15,7 +15,10 @@ internal static class CommittedFile /// The exact bytes of HEAD:<repoRelativePath>. /// A repository-relative path with forward slashes. - /// git is missing, the tree is not a git checkout, or the path is not committed. + /// + /// git is missing, the tree is not a git checkout, or the path is not + /// committed. + /// public static async Task ReadBytesAsync(string repoRelativePath) { ArgumentException.ThrowIfNullOrWhiteSpace(repoRelativePath); @@ -61,7 +64,8 @@ public static async Task ReadBytesAsync(string repoRelativePath) catch (OperationCanceledException exception) when (cancellation.IsCancellationRequested) { process.Kill(true); - throw new TimeoutException($"'git cat-file blob HEAD:{repoRelativePath}' did not exit within {Timeout}.", exception); + throw new TimeoutException($"'git cat-file blob HEAD:{repoRelativePath}' did not exit within {Timeout}.", + exception); } if (process.ExitCode != 0) diff --git a/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs b/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs index d23b845e..1f86b27e 100644 --- a/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs +++ b/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs @@ -21,21 +21,23 @@ namespace CheatEngine.SDK.Tests.Infrastructure; /// its feed and never packs: in the CI Release leg it is the .nupkg that is uploaded as /// nuget-package, attested and published, so these tests are evidence about the shipped file. Otherwise, /// outside CI, it packs src/CheatEngine.SDK/CheatEngine.SDK.csproj itself: the facts then describe the -/// working tree, not a shipped file. Under CI=true a missing variable is an error, never a silent self-pack. +/// working tree, not a shipped file. Under CI=true a missing variable is an error, never a silent +/// self-pack. /// /// -/// Every consumer restore is pointed (dotnet restore --packages) at , a -/// directory scoped to this fixture's own _tempRoot, never the machine-wide global-packages folder -/// (typically %USERPROFILE%\.nuget\packages). This matters because every pack in one session gets the SAME -/// MinVer-derived version (git commit height only changes on a new commit), and NuGet treats a package id+version -/// already extracted in the global-packages folder as immutable: a later restore against a freshly re-packed -/// .nupkg with different content but the same version would silently reuse whatever was extracted there by -/// an earlier run - this fixture's own previous run, a developer's manual restore, or another parallel build on -/// the same machine - with no error or warning. That would defeat this project's whole point (verifying the -/// freshly packed umbrella, not some earlier one) for every consumer-based assertion -/// (EntryPointTests, BuildPropertyDefaultsTests); only the nuspec/file-list tests, which read the -/// .nupkg directly via , would be unaffected. Starting every fixture run from an -/// empty, run-scoped packages directory removes the sharing that makes that possible. +/// Every consumer restore is pointed (dotnet restore --packages) at , a +/// directory scoped to this fixture's own _tempRoot, never the machine-wide global-packages folder +/// (typically %USERPROFILE%\.nuget\packages). This matters because every pack in one session gets the SAME +/// MinVer-derived version (git commit height only changes on a new commit), and NuGet treats a package id+version +/// already extracted in the global-packages folder as immutable: a later restore against a freshly re-packed +/// .nupkg with different content but the same version would silently reuse whatever was extracted there by +/// an earlier run - this fixture's own previous run, a developer's manual restore, or another parallel build on +/// the same machine - with no error or warning. That would defeat this project's whole point (verifying the +/// freshly packed umbrella, not some earlier one) for every consumer-based assertion +/// (EntryPointTests, BuildPropertyDefaultsTests); only the nuspec/file-list tests, which read the +/// .nupkg directly via , would be unaffected. Starting every fixture run from +/// an +/// empty, run-scoped packages directory removes the sharing that makes that possible. /// /// public sealed class PackagedUmbrellaFixture : IAsyncLifetime diff --git a/tests/CheatEngine.SDK.Tests/Packaging/BridgeAuditManifestTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/BridgeAuditManifestTests.cs index 560a965b..b1070ba1 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/BridgeAuditManifestTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/BridgeAuditManifestTests.cs @@ -9,7 +9,8 @@ namespace CheatEngine.SDK.Tests.Packaging; /// -/// native/cheatengine-sdk-lua-bridge/bridge-audit-manifest.json records the audited bridge: the SHA-256 of its two +/// native/cheatengine-sdk-lua-bridge/bridge-audit-manifest.json records the audited bridge: the SHA-256 of its +/// two /// LF-pinned build inputs, the fingerprint embedded in the DLL, the DLL SHA-256, its PE facts and the pinned xmake /// version. The DLL facts are read from the committed blob (git cat-file), never from the working tree: /// CI overwrites the working-tree DLL with the one it builds, and a byte difference between the two is drift that CI @@ -39,9 +40,11 @@ public async Task Bridge_audit_manifest_records_the_current_source_hashes_and_fi string buildSha256 = Sha256(File.ReadAllBytes(BridgeInput(BuildFile))); string message = await StaleMessageAsync().ConfigureAwait(true); - Assert.True(string.Equals(sourceSha256, (string?) manifest["source"]?["hashes"]?[SourceFile], StringComparison.Ordinal), + Assert.True( + string.Equals(sourceSha256, (string?) manifest["source"]?["hashes"]?[SourceFile], StringComparison.Ordinal), message); - Assert.True(string.Equals(buildSha256, (string?) manifest["source"]?["hashes"]?[BuildFile], StringComparison.Ordinal), + Assert.True( + string.Equals(buildSha256, (string?) manifest["source"]?["hashes"]?[BuildFile], StringComparison.Ordinal), message); Assert.True(string.Equals($"{sourceSha256}:{buildSha256}", (string?) manifest["source"]?["fingerprint"], StringComparison.Ordinal), message); @@ -56,8 +59,10 @@ public async Task Bridge_audit_manifest_matches_the_committed_bridge_blob() JsonNode? asset = manifest["nativeAsset"]; Assert.True(string.Equals(committed.Sha256, (string?) asset?["sha256"], StringComparison.Ordinal), message); - Assert.True(string.Equals(committed.Format, (string?) asset?["pe"]?["format"], StringComparison.Ordinal), message); - Assert.True(string.Equals(committed.Machine, (string?) asset?["pe"]?["machine"], StringComparison.Ordinal), message); + Assert.True(string.Equals(committed.Format, (string?) asset?["pe"]?["format"], StringComparison.Ordinal), + message); + Assert.True(string.Equals(committed.Machine, (string?) asset?["pe"]?["machine"], StringComparison.Ordinal), + message); Assert.True(committed.IsDll && (bool?) asset?["pe"]?["isDll"] == true, message); Assert.True(SetEquals(committed.Exports, Strings(asset?["exports"])), message); Assert.True(SetEquals(committed.Imports, Strings(asset?["imports"])), message); @@ -99,7 +104,8 @@ public void Bridge_audit_manifest_asset_path_exists_with_exact_case() } } - Assert.True(match is not null, $"'{segment}' of asset '{asset}' does not exist with that exact case in '{current}'."); + Assert.True(match is not null, + $"'{segment}' of asset '{asset}' does not exist with that exact case in '{current}'."); current = match!; } diff --git a/tests/CheatEngine.SDK.Tests/Packaging/CleanConsumerIsolationTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/CleanConsumerIsolationTests.cs index 53e8f75e..fb9fe1a3 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/CleanConsumerIsolationTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/CleanConsumerIsolationTests.cs @@ -51,7 +51,8 @@ public void Consumer_manifests_contain_no_repository_or_developer_path() { string path = Path.Combine(folder, manifest); Assert.True(File.Exists(path), $"'{path}' was not produced."); - foreach (string leak in ConsumerManifestRules.WorkspaceLeaks(File.ReadAllText(path), RepositoryLayout.Root)) + foreach (string leak in ConsumerManifestRules.WorkspaceLeaks(File.ReadAllText(path), + RepositoryLayout.Root)) { offenders.Add($"{path}: {leak}"); } diff --git a/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRuleTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRuleTests.cs index 2b440d4e..aff6c4f3 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRuleTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRuleTests.cs @@ -3,13 +3,17 @@ namespace CheatEngine.SDK.Tests.Packaging; /// -/// Proves, on synthetic manifests and without the packaging fixture, that each rule +/// Proves, on synthetic manifests and without the packaging fixture, that each +/// rule /// accepts a clean package consumer and rejects the workspace leak it exists for. Runs in both CI legs. /// public sealed class ConsumerManifestRuleTests { private const string Version = "2.0.0-alpha.0.18"; - private const string Sha512 = "n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA=="; + + private const string Sha512 = + "n7nHqZ8vzo7Vf20jF0fkh/jUtR3yo1TwRGpXE7ERxZeJ4C5S/Nsft4lqOg7zGwfsD5Nh9tTVgdw4PrybJRF0gA=="; + private const string Root = @"D:\a\CheatEngine.SDK\CheatEngine.SDK"; [Fact] diff --git a/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRules.cs b/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRules.cs index 0fdc6775..196d82d6 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRules.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/ConsumerManifestRules.cs @@ -3,10 +3,12 @@ namespace CheatEngine.SDK.Tests.Packaging; /// -/// What a clean package consumer's manifests must and must not say (audit ch.21: a consumer that works thanks to a file +/// What a clean package consumer's manifests must and must not say (audit ch.21: a consumer that works thanks to a +/// file /// found in the development folder is not a qualified consumer). Pure functions of the manifest text, each returning /// one message per problem, so applies them to the real consumer and -/// proves on synthetic manifests that each rule fails on the leak it exists for. +/// proves on synthetic manifests that each rule fails on the leak it exists +/// for. /// internal static class ConsumerManifestRules { @@ -46,7 +48,8 @@ public static List SdkLibraryProblems(string depsJson, string packageVer if (!string.Equals(sha512, expectedSha512, StringComparison.Ordinal)) { - problems.Add($"library '{key}' has sha512 '{sha512}', expected '{expectedSha512}' (the package under test)"); + problems.Add( + $"library '{key}' has sha512 '{sha512}', expected '{expectedSha512}' (the package under test)"); } return problems; diff --git a/tests/CheatEngine.SDK.Tests/Packaging/LivePluginClosureTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/LivePluginClosureTests.cs index 75e41954..e26e281a 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/LivePluginClosureTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/LivePluginClosureTests.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Globalization; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -59,7 +60,8 @@ public void Live_plugin_output_closure_matches_the_workspace_bridge(string proje foreach (JsonProperty library in target.EnumerateObject()) { string name = library.Name[..library.Name.IndexOf('/', StringComparison.Ordinal)]; - if (name.StartsWith("CheatEngine.SDK.", StringComparison.Ordinal) && !string.Equals(name, project, StringComparison.Ordinal)) + if (name.StartsWith("CheatEngine.SDK.", StringComparison.Ordinal) && + !string.Equals(name, project, StringComparison.Ordinal)) { sdkLibraries.Add(name); } @@ -76,7 +78,8 @@ public void Live_plugin_output_closure_matches_the_workspace_bridge(string proje Assert.Superset(new HashSet(s_bootstrapLibraries, StringComparer.Ordinal), sdkLibraries); foreach (string file in promised) { - Assert.True(File.Exists(Path.Combine(folder, file)), $"{project}.deps.json promises {file}, which is not in {folder}."); + Assert.True(File.Exists(Path.Combine(folder, file)), + $"{project}.deps.json promises {file}, which is not in {folder}."); } foreach (string assembly in Directory.EnumerateFiles(folder, "CheatEngine.SDK.*.dll")) @@ -86,7 +89,8 @@ public void Live_plugin_output_closure_matches_the_workspace_bridge(string proje } // The runtime policy is the one the managed hostfxr profile loads: net10.0 on the shared Microsoft.NETCore.App. - using JsonDocument runtimeConfig = JsonDocument.Parse(File.ReadAllBytes(Path.Combine(folder, project + ".runtimeconfig.json"))); + using JsonDocument runtimeConfig = + JsonDocument.Parse(File.ReadAllBytes(Path.Combine(folder, project + ".runtimeconfig.json"))); JsonElement options = runtimeConfig.RootElement.GetProperty("runtimeOptions"); Assert.Equal("net10.0", options.GetProperty("tfm").GetString()); Assert.Equal("Microsoft.NETCore.App", options.GetProperty("framework").GetProperty("name").GetString()); @@ -99,7 +103,8 @@ public void Live_plugin_output_closure_matches_the_workspace_bridge(string proje foreach (string file in Directory.EnumerateFiles(folder, "*.dll")) { string name = Path.GetFileName(file); - Assert.False(name.StartsWith("lua", StringComparison.OrdinalIgnoreCase), $"{folder} deploys a Lua runtime, {name}."); + Assert.False(name.StartsWith("lua", StringComparison.OrdinalIgnoreCase), + $"{folder} deploys a Lua runtime, {name}."); } } @@ -129,7 +134,9 @@ public void Live_plugin_entry_point_is_present_unless_listed_as_a_pending_fix(st /// private static string BundleFolder(string project) { - string configuration = new DirectoryInfo(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)).Name; + string configuration = + new DirectoryInfo(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar)).Name; string folder = RepositoryLayout.PathOf($"artifacts/bin/{project}/{configuration}"); Assert.True(File.Exists(Path.Combine(folder, project + ".dll")), $"{project} has no {configuration} output in {folder}: build CheatEngine.SDK.slnx -c {configuration} first."); @@ -159,10 +166,15 @@ private static string BundleFolder(string project) continue; } - MethodSignature decoded = method.DecodeSignature(SignatureNames.Instance, genericContext: null); - string access = (method.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public ? "public" : "non-public"; - string binding = (method.Attributes & MethodAttributes.Static) != MethodAttributes.PrivateScope ? "static" : "instance"; - return $"{access} {binding} {decoded.ReturnType} CEPluginInitialize({string.Join(", ", decoded.ParameterTypes)})"; + MethodSignature decoded = method.DecodeSignature(SignatureNames.Instance, null); + string access = (method.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public + ? "public" + : "non-public"; + string binding = (method.Attributes & MethodAttributes.Static) != MethodAttributes.PrivateScope + ? "static" + : "instance"; + return + $"{access} {binding} {decoded.ReturnType} CEPluginInitialize({string.Join(", ", decoded.ParameterTypes)})"; } } @@ -202,7 +214,8 @@ public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle ha return reader.GetString(reference.Namespace) + "." + reader.GetString(reference.Name); } - public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, + TypeSpecificationHandle handle, byte rawTypeKind) { return "typespec"; } @@ -234,12 +247,12 @@ public string GetGenericInstantiation(string genericType, ImmutableArray public string GetGenericMethodParameter(object? genericContext, int index) { - return "!!" + index.ToString(System.Globalization.CultureInfo.InvariantCulture); + return "!!" + index.ToString(CultureInfo.InvariantCulture); } public string GetGenericTypeParameter(object? genericContext, int index) { - return "!" + index.ToString(System.Globalization.CultureInfo.InvariantCulture); + return "!" + index.ToString(CultureInfo.InvariantCulture); } public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) diff --git a/tests/CheatEngine.SDK.Tests/Packaging/PackageProvenanceTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/PackageProvenanceTests.cs index f970bf89..e743833b 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/PackageProvenanceTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/PackageProvenanceTests.cs @@ -7,7 +7,8 @@ namespace CheatEngine.SDK.Tests.Packaging; /// /// Which file every other packaging fact is about. In the CI Release leg the fixture must consume the exact /// .nupkg named by (the file uploaded as -/// nuget-package, attested and published) and never pack; only a local run outside CI may pack the working tree. +/// nuget-package, attested and published) and never pack; only a local run outside CI may pack the working +/// tree. /// The identity of the consumed file is written to the test output, so it lands in the TRX next to the results. /// [Collection(PackagedUmbrellaSuite.Name)] diff --git a/tests/CheatEngine.SDK.Tests/Packaging/PackagedUmbrellaTraitTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/PackagedUmbrellaTraitTests.cs index 18b1de83..6677f1fa 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/PackagedUmbrellaTraitTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/PackagedUmbrellaTraitTests.cs @@ -5,7 +5,8 @@ namespace CheatEngine.SDK.Tests.Packaging; /// -/// The Debug CI leg excludes the packaging fixture with --filter-not-trait "Category=Packaging", and the Release +/// The Debug CI leg excludes the packaging fixture with --filter-not-trait "Category=Packaging", and the +/// Release /// leg hands it the exact package. That only works if the trait and the collection coincide: a collection class /// without the trait would start a pack in the Debug leg (or fail it under CI=true), and a traited class /// outside the collection would silently drop out of the Debug leg. Read through , @@ -87,7 +88,8 @@ private static List TestTypes() private static bool HasTestMethod(Type type) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | + foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | + BindingFlags.Static | BindingFlags.DeclaredOnly)) { foreach (CustomAttributeData attribute in method.GetCustomAttributesData()) @@ -155,7 +157,8 @@ private static bool HasPackagingTrait(MemberInfo member) private static bool HasPackagingTraitOnAMethod(Type type) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | + foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | + BindingFlags.Static | BindingFlags.DeclaredOnly)) { if (HasPackagingTrait(method)) diff --git a/tests/CheatEngine.SDK.Tests/Packaging/SupplyChainPackageTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/SupplyChainPackageTests.cs index f134c846..6cbffa28 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/SupplyChainPackageTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/SupplyChainPackageTests.cs @@ -46,11 +46,12 @@ public void Package_embeds_an_spdx_2_2_sbom_describing_itself() Assert.Equal(NuspecMetadata("id"), package.GetProperty("name").GetString()); Assert.Equal(fixture.PackageVersion, package.GetProperty("versionInfo").GetString()); Assert.Equal(fixture.PackageVersion, NuspecMetadata("version")); - Assert.StartsWith($"{RepositoryUrl}/", root.GetProperty("documentNamespace").GetString(), StringComparison.Ordinal); + Assert.StartsWith($"{RepositoryUrl}/", root.GetProperty("documentNamespace").GetString(), + StringComparison.Ordinal); // The sidecar checksum the SBOM tool writes next to the manifest matches it. string sidecar = Encoding.ASCII.GetString(ReadEntry(archive, SbomChecksumEntry)).Trim(); - Assert.Equal(Sha256(manifest), sidecar, ignoreCase: true); + Assert.Equal(Sha256(manifest), sidecar, true); } [Fact] @@ -70,11 +71,12 @@ public void Sbom_lists_every_shipped_assembly_and_the_native_bridge_with_its_sha } } - Assert.True(shipped.Count >= 13, $"Expected the 7 libraries, 5 components and the bridge, found {shipped.Count}."); + Assert.True(shipped.Count >= 13, + $"Expected the 7 libraries, 5 components and the bridge, found {shipped.Count}."); foreach (string entry in shipped) { Assert.True(sbomSha256.TryGetValue(entry, out string? declared), $"The SBOM does not list '{entry}'."); - Assert.Equal(Sha256(ReadEntry(archive, entry)), declared, ignoreCase: true); + Assert.Equal(Sha256(ReadEntry(archive, entry)), declared, true); } } @@ -156,7 +158,8 @@ public void Embedded_libraries_carry_source_link_to_the_repository_commit() MetadataReader pdb = pdbProvider.GetMetadataReader(); string? sourceLink = null; - foreach (CustomDebugInformationHandle handle in pdb.GetCustomDebugInformation(EntityHandle.ModuleDefinition)) + foreach (CustomDebugInformationHandle handle in + pdb.GetCustomDebugInformation(EntityHandle.ModuleDefinition)) { CustomDebugInformation information = pdb.GetCustomDebugInformation(handle); if (pdb.GetGuid(information.Kind) == s_sourceLinkKind) @@ -244,6 +247,6 @@ private static Version CoreVersion(string packageVersion) return Version.Parse(core); } - [GeneratedRegex("^[0-9a-f]{40}$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)] + [GeneratedRegex("^[0-9a-f]{40}$", RegexOptions.CultureInvariant, 1000)] private static partial Regex CommitSha(); } diff --git a/tests/CheatEngine.SDK.Tests/Packaging/UmbrellaPackageSourceTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/UmbrellaPackageSourceTests.cs index 427e3d57..e2f50a38 100644 --- a/tests/CheatEngine.SDK.Tests/Packaging/UmbrellaPackageSourceTests.cs +++ b/tests/CheatEngine.SDK.Tests/Packaging/UmbrellaPackageSourceTests.cs @@ -82,7 +82,8 @@ public void Missing_variable_under_ci_fails_with_an_actionable_message(string? v Assert.StartsWith(UmbrellaPackage.PrebuiltPackageVariable + " is not set while CI=true.", failure.Message, StringComparison.Ordinal); Assert.Contains("--filter-not-trait \"Category=Packaging\"", failure.Message, StringComparison.Ordinal); - Assert.Contains("tests/CheatEngine.SDK.Tests/README.md#run-the-tests", failure.Message, StringComparison.Ordinal); + Assert.Contains("tests/CheatEngine.SDK.Tests/README.md#run-the-tests", failure.Message, + StringComparison.Ordinal); } [Theory] diff --git a/tests/CheatEngine.SDK.Tests/README.md b/tests/CheatEngine.SDK.Tests/README.md index 6da39775..ef0aa524 100644 --- a/tests/CheatEngine.SDK.Tests/README.md +++ b/tests/CheatEngine.SDK.Tests/README.md @@ -16,14 +16,16 @@ A project reference proves that the source compiles, not that the installed pack 1. One collection fixture (`PackagedUmbrellaFixture`) puts one `CheatEngine.SDK` package into a temporary local feed. Its origin is decided first, by `UmbrellaPackageSource`: - - `CESDK_PACKAGED_UMBRELLA_NUPKG` set: it must be the absolute path of a `CheatEngine.SDK..nupkg` file. The - fixture copies exactly that file and never packs. This is the CI Release leg: it packs once, passes the packed file, - and the same file is uploaded as `nuget-package`, attested and published, so these tests are evidence about the - shipped file. - - Unset while `CI=true`: the fixture fails at once with an actionable message. A CI run must test the file it ships, - and the Debug leg excludes these tests with `--filter-not-trait "Category=Packaging"`. - - Unset outside CI: the fixture packs `src/CheatEngine.SDK/CheatEngine.SDK.csproj` in Release itself. That package is - built from your working tree; it is evidence about the source (C1/C2), not about a file that was ever shipped. + - `CESDK_PACKAGED_UMBRELLA_NUPKG` set: it must be the absolute path of a `CheatEngine.SDK..nupkg` file. The + fixture copies exactly that file and never packs. This is the CI Release leg: it packs once, passes the packed + file, + and the same file is uploaded as `nuget-package`, attested and published, so these tests are evidence about the + shipped file. + - Unset while `CI=true`: the fixture fails at once with an actionable message. A CI run must test the file it ships, + and the Debug leg excludes these tests with `--filter-not-trait "Category=Packaging"`. + - Unset outside CI: the fixture packs `src/CheatEngine.SDK/CheatEngine.SDK.csproj` in Release itself. That package + is + built from your working tree; it is evidence about the source (C1/C2), not about a file that was ever shipped. The package id, that project path, the variable name, the `Packaging` category and the lower-cased id NuGet uses as the extraction folder name live in one place, `UmbrellaPackage`. Every class of the `PackagedUmbrellaSuite` diff --git a/tests/CheatEngine.SDK.Tests/packages.lock.json b/tests/CheatEngine.SDK.Tests/packages.lock.json index fe279b25..b254c5b5 100644 --- a/tests/CheatEngine.SDK.Tests/packages.lock.json +++ b/tests/CheatEngine.SDK.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.283, )", - "resolved": "3.0.283", - "contentHash": "8UKUK7wiL+DMA/c8mGPK9J24wLKDZ5DHKDv38GlyeJNkza9Q1yQziudEt1Op7/+0CL1vVi41PhACj1e6948f7w==" + "requested": "[3.0.290, )", + "resolved": "3.0.290", + "contentHash": "vdSXgMEPil67EFwkcicjSCMnB7TdhUNiR1Fjw1P8MN+UeX6yP8wTb7cNT20H/ejHuUxkrxQS1Rz4QcJuMLmCkA==" }, "Microsoft.Testing.Extensions.CodeCoverage": { "type": "Direct",