From dcbd3ac8418ee69c53ba34d871017243c2d9866b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:12:14 +0000 Subject: [PATCH] test: add comprehensive C# and Python unit tests for the SDK Agent-Logs-Url: https://github.com/PyMCU/pymcu-sdk/sessions/1eb50c7b-0c67-4b08-83f1-63aac2c394ff Co-authored-by: begeistert <36460223+begeistert@users.noreply.github.com> --- PyMCU.SDK.slnx | 3 + pyproject.toml | 6 + .../Analysis/DynamicStackAllocatorTests.cs | 184 +++++++++ .../Backend/Analysis/StackAllocatorTests.cs | 235 +++++++++++ .../Backend/License/LicenseResultTests.cs | 64 +++ .../Backend/License/LicenseValidatorTests.cs | 169 ++++++++ .../Serialization/IrSerializerTests.cs | 364 ++++++++++++++++++ tests/csharp/IR/DataTypeTests.cs | 168 ++++++++ tests/csharp/IR/TackyTests.cs | 292 ++++++++++++++ tests/csharp/PyMCU.Backend.SDK.Tests.csproj | 29 ++ tests/python/conftest.py | 2 + tests/python/test_backend_plugin.py | 160 ++++++++ tests/python/test_base_tool.py | 299 ++++++++++++++ tests/python/test_toolchain_plugin.py | 156 ++++++++ 14 files changed, 2131 insertions(+) create mode 100644 tests/csharp/Backend/Analysis/DynamicStackAllocatorTests.cs create mode 100644 tests/csharp/Backend/Analysis/StackAllocatorTests.cs create mode 100644 tests/csharp/Backend/License/LicenseResultTests.cs create mode 100644 tests/csharp/Backend/License/LicenseValidatorTests.cs create mode 100644 tests/csharp/Backend/Serialization/IrSerializerTests.cs create mode 100644 tests/csharp/IR/DataTypeTests.cs create mode 100644 tests/csharp/IR/TackyTests.cs create mode 100644 tests/csharp/PyMCU.Backend.SDK.Tests.csproj create mode 100644 tests/python/conftest.py create mode 100644 tests/python/test_backend_plugin.py create mode 100644 tests/python/test_base_tool.py create mode 100644 tests/python/test_toolchain_plugin.py diff --git a/PyMCU.SDK.slnx b/PyMCU.SDK.slnx index 5b84c50..a1b98d9 100644 --- a/PyMCU.SDK.slnx +++ b/PyMCU.SDK.slnx @@ -2,4 +2,7 @@ + + + diff --git a/pyproject.toml b/pyproject.toml index 7078455..28f90f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,9 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = ["src/python/pymcu"] +[tool.pytest.ini_options] +testpaths = ["tests/python"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + diff --git a/tests/csharp/Backend/Analysis/DynamicStackAllocatorTests.cs b/tests/csharp/Backend/Analysis/DynamicStackAllocatorTests.cs new file mode 100644 index 0000000..d6b86d4 --- /dev/null +++ b/tests/csharp/Backend/Analysis/DynamicStackAllocatorTests.cs @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +// PyMCU Backend SDK — Unit tests for DynamicStackAllocator. + +using FluentAssertions; +using PyMCU.Backend.Analysis; +using PyMCU.IR; +using Xunit; + +namespace PyMCU.Backend.SDK.Tests.Backend.Analysis; + +public class DynamicStackAllocatorTests +{ + private static Function MakeFunc(string name, IList body, params string[] parms) + { + return new Function + { + Name = name, + Params = [.. parms], + Body = [.. body] + }; + } + + // ── Word size and reserved-top defaults ─────────────────────────────── + + [Fact] + public void Allocate_EmptyFunction_TotalSizeIsAlignedReserved() + { + // No locals, no params: totalSize = reservedTop rounded up to 16. + // reservedTop=8 → -currentOffset starts at 8, totalSize=8 → rounds up to 16. + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 8); + var func = MakeFunc("main", [new Return(new NoneVal())]); + var (offsets, total) = alloc.Allocate(func); + + offsets.Should().BeEmpty(); + total.Should().Be(16); // 8 rounded up to next multiple of 16 + } + + // ── Single variable allocation ───────────────────────────────────────── + + [Fact] + public void Allocate_OneVariable_HasNegativeOffset() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 8); + var func = MakeFunc("main", [ + new Copy(new Constant(1), new Variable("x")), + new Return(new Variable("x")) + ]); + var (offsets, total) = alloc.Allocate(func); + + offsets.Should().ContainKey("x"); + offsets["x"].Should().BeNegative(); // frame grows downwards + total.Should().BeGreaterThan(0); + } + + // ── Parameters come first ───────────────────────────────────────────── + + [Fact] + public void Allocate_Params_AllocatedBeforeBodyLocals() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 8); + var func = MakeFunc("add", [ + new Binary(BinaryOp.Add, new Variable("a"), new Variable("b"), new Variable("result")), + new Return(new Variable("result")) + ], "a", "b"); + + var (offsets, _) = alloc.Allocate(func); + + offsets.Should().ContainKeys("a", "b", "result"); + // Params a and b are allocated first → more-negative offsets (DynamicStackAllocator + // grows downward, so first-allocated = highest address = least-negative offset). + // Params come before body locals, so params have a HIGHER (less-negative) offset. + offsets["result"].Should().BeLessThan(offsets["a"]); + offsets["result"].Should().BeLessThan(offsets["b"]); + } + + // ── Alignment: total size is always a multiple of 16 ───────────────── + + [Theory] + [InlineData(1)] + [InlineData(3)] + [InlineData(5)] + public void Allocate_TotalSize_IsAlwaysMultipleOf16(int numVars) + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 0); + var body = Enumerable.Range(0, numVars) + .Select(i => (Instruction)new Copy(new Constant(i), new Variable($"v{i}"))) + .Append(new Return(new NoneVal())) + .ToList(); + + var func = MakeFunc("main", body); + var (_, total) = alloc.Allocate(func); + + (total % 16).Should().Be(0); + } + + // ── Each variable gets a unique offset ──────────────────────────────── + + [Fact] + public void Allocate_MultipleVars_AllGetDistinctOffsets() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 0); + var func = MakeFunc("main", [ + new Copy(new Constant(1), new Variable("a")), + new Copy(new Constant(2), new Variable("b")), + new Copy(new Constant(3), new Variable("c")), + new Return(new NoneVal()) + ]); + + var (offsets, _) = alloc.Allocate(func); + + var vals = new[] { offsets["a"], offsets["b"], offsets["c"] }; + vals.Should().OnlyHaveUniqueItems(); + } + + // ── Same variable in multiple instructions — allocated once ─────────── + + [Fact] + public void Allocate_SameVarReferencedMultipleTimes_AllocatedOnce() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 0); + var func = MakeFunc("main", [ + new Copy(new Constant(1), new Variable("x")), + new Copy(new Variable("x"), new Variable("y")), + new Return(new Variable("x")) + ]); + + var (offsets, _) = alloc.Allocate(func); + + offsets.Should().ContainKey("x"); + offsets.Keys.Count(k => k == "x").Should().Be(1); // only one entry for x + } + + // ── Temporaries are also allocated ──────────────────────────────────── + + [Fact] + public void Allocate_Temporaries_AreIncluded() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 0); + var func = MakeFunc("main", [ + new Binary(BinaryOp.Add, new Variable("a"), new Variable("b"), new Temporary("t1")), + new Return(new Temporary("t1")) + ]); + + var (offsets, _) = alloc.Allocate(func); + + offsets.Should().ContainKey("t1"); + } + + // ── Bit instructions ──────────────────────────────────────────────────── + + [Fact] + public void Allocate_BitInstructions_RegisterVars() + { + var alloc = new DynamicStackAllocator(wordSize: 4, reservedTop: 0); + var func = MakeFunc("main", [ + new BitSet(new Variable("port"), 3), + new BitClear(new Variable("port"), 5), + new BitCheck(new Variable("port"), 2, new Temporary("t1")), + new BitWrite(new Variable("port"), 4, new Variable("val")), + new Return(new NoneVal()) + ]); + + var (offsets, _) = alloc.Allocate(func); + + offsets.Should().ContainKeys("port", "t1", "val"); + } + + // ── Custom word size propagates correctly ───────────────────────────── + + [Fact] + public void Allocate_CustomWordSize_OffsetsAreSeparatedByWordSize() + { + var alloc = new DynamicStackAllocator(wordSize: 2, reservedTop: 0); + var func = MakeFunc("main", [ + new Copy(new Constant(1), new Variable("a")), + new Copy(new Constant(2), new Variable("b")), + new Return(new NoneVal()) + ]); + + var (offsets, _) = alloc.Allocate(func); + + Math.Abs(offsets["a"] - offsets["b"]).Should().Be(2); + } +} diff --git a/tests/csharp/Backend/Analysis/StackAllocatorTests.cs b/tests/csharp/Backend/Analysis/StackAllocatorTests.cs new file mode 100644 index 0000000..1e71782 --- /dev/null +++ b/tests/csharp/Backend/Analysis/StackAllocatorTests.cs @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +// PyMCU Backend SDK — Unit tests for StackAllocator. + +using FluentAssertions; +using PyMCU.Backend.Analysis; +using PyMCU.IR; +using Xunit; + +namespace PyMCU.Backend.SDK.Tests.Backend.Analysis; + +public class StackAllocatorTests +{ + private static ProgramIR MakeProgram( + IEnumerable? globals = null, + IEnumerable? funcs = null) + { + var prog = new ProgramIR(); + if (globals != null) prog.Globals.AddRange(globals); + if (funcs != null) prog.Functions.AddRange(funcs); + return prog; + } + + private static Function MakeFunc(string name, IList body, bool isInterrupt = false, int vector = 0, IList? parms = null) + { + return new Function + { + Name = name, + Params = parms != null ? [.. parms] : [], + Body = [.. body], + IsInterrupt = isInterrupt, + InterruptVector = vector + }; + } + + // ── Global allocation ────────────────────────────────────────────────── + + [Fact] + public void Allocate_NoFunctions_AllocatesGlobals() + { + var prog = MakeProgram(globals: [ + new Variable("a", DataType.UINT8), + new Variable("b", DataType.UINT16) + ]); + + var allocator = new StackAllocator(); + var (offsets, maxStack) = allocator.Allocate(prog); + + offsets["a"].Should().Be(0); + offsets["b"].Should().Be(1); // UINT8 occupies 1 byte + maxStack.Should().Be(3); // 1 + 2 + } + + // ── Single function — local variables ───────────────────────────────── + + [Fact] + public void Allocate_SingleFunction_AssignsSequentialOffsets() + { + var prog = MakeProgram(funcs: [MakeFunc("main", [ + new Copy(new Constant(1), new Variable("x")), + new Copy(new Constant(2), new Variable("y")), + new Return(new Variable("x")) + ])]); + + var allocator = new StackAllocator(); + var (offsets, maxStack) = allocator.Allocate(prog); + + offsets.Should().ContainKey("x"); + offsets.Should().ContainKey("y"); + // x and y are 1-byte locals starting at 0 + offsets["x"].Should().Be(0); + offsets["y"].Should().Be(1); + maxStack.Should().BeGreaterThanOrEqualTo(2); + } + + // ── Parameter registration ───────────────────────────────────────────── + + [Fact] + public void Allocate_FunctionParams_AreRegisteredAsLocals() + { + // "add" is called from main — params get allocated as part of the call graph. + var prog = MakeProgram(funcs: [ + MakeFunc("main", [ + new Call("add", [new Constant(1), new Constant(2)], new Variable("out")), + new Return(new Variable("out")) + ]), + MakeFunc("add", [ + new Binary(BinaryOp.Add, new Variable("a"), new Variable("b"), new Variable("result")), + new Return(new Variable("result")) + ], parms: ["a", "b"]) + ]); + + var allocator = new StackAllocator(); + var (offsets, _) = allocator.Allocate(prog); + + // params a and b should be in the offset table + offsets.Should().ContainKey("a"); + offsets.Should().ContainKey("b"); + } + + // ── Globals vs locals: globals must not be re-allocated in functions ─── + + [Fact] + public void Allocate_GlobalNotReallocatedAsLocal() + { + var prog = MakeProgram( + globals: [new Variable("led", DataType.UINT8)], + funcs: [MakeFunc("main", [ + new Copy(new Constant(1), new Variable("led")), // led is global + new Copy(new Constant(0), new Variable("local")), + new Return(new NoneVal()) + ])]); + + var allocator = new StackAllocator(); + var (offsets, maxStack) = allocator.Allocate(prog); + + // led is global at offset 0 + offsets["led"].Should().Be(0); + // local starts right after the globals (offset 1) + offsets["local"].Should().Be(1); + } + + // ── Call graph: caller + callee stack frames are sequenced ───────────── + + [Fact] + public void Allocate_CallerCallee_SequencesFrames() + { + var callee = MakeFunc("helper", [ + new Return(new Variable("ret")) + ], parms: ["ret"]); + + var caller = MakeFunc("main", [ + new Call("helper", [new Constant(1)], new Variable("res")), + new Return(new Variable("res")) + ]); + + var prog = MakeProgram(funcs: [caller, callee]); + var allocator = new StackAllocator(); + var (offsets, maxStack) = allocator.Allocate(prog); + + // caller's locals come first, then callee's locals start at a higher offset + offsets.Should().ContainKey("res"); + offsets.Should().ContainKey("ret"); + offsets["ret"].Should().BeGreaterThan(offsets["res"]); + } + + // ── Interrupt functions are also allocated ───────────────────────────── + + [Fact] + public void Allocate_InterruptFunction_GetsOffsets() + { + var isr = MakeFunc("isr_tim0", [ + new Copy(new Constant(0), new Variable("tick")), + new Return(new NoneVal()) + ], isInterrupt: true, vector: 1); + + var main = MakeFunc("main", [new Return(new NoneVal())]); + + var prog = MakeProgram(funcs: [main, isr]); + var allocator = new StackAllocator(); + var (offsets, _) = allocator.Allocate(prog); + + offsets.Should().ContainKey("tick"); + } + + // ── VariableSizes is populated ──────────────────────────────────────── + + [Fact] + public void Allocate_VariableSizes_ReflectsDataTypes() + { + var prog = MakeProgram(funcs: [MakeFunc("main", [ + new Copy(new Constant(0), new Variable("b8", DataType.UINT8)), + new Copy(new Constant(0), new Variable("b16", DataType.UINT16)), + new Copy(new Constant(0), new Variable("b32", DataType.UINT32)), + new Return(new NoneVal()) + ])]); + + var allocator = new StackAllocator(); + allocator.Allocate(prog); + + allocator.VariableSizes["b8"].Should().Be(1); + allocator.VariableSizes["b16"].Should().Be(2); + allocator.VariableSizes["b32"].Should().Be(4); + } + + // ── BitSet/BitClear/BitCheck/BitWrite variables are captured ────────── + + [Fact] + public void Allocate_BitInstructions_RegisterVars() + { + var prog = MakeProgram(funcs: [MakeFunc("main", [ + new BitSet(new Variable("port"), 3), + new BitClear(new Variable("port"), 5), + new BitCheck(new Variable("port"), 2, new Temporary("t1")), + new BitWrite(new Variable("port"), 4, new Variable("val")), + new Return(new NoneVal()) + ])]); + + var allocator = new StackAllocator(); + var (offsets, _) = allocator.Allocate(prog); + + offsets.Should().ContainKey("port"); + offsets.Should().ContainKey("t1"); + offsets.Should().ContainKey("val"); + } + + // ── Array instructions register array name with correct total size ───── + + [Fact] + public void Allocate_ArrayLoad_RegistersArrayWithTotalByteSize() + { + var prog = MakeProgram(funcs: [MakeFunc("main", [ + new ArrayLoad("buf", new Variable("i"), new Temporary("t1"), DataType.UINT8, 10), + new Return(new NoneVal()) + ])]); + + var allocator = new StackAllocator(); + allocator.Allocate(prog); + + allocator.VariableSizes["buf"].Should().Be(10); // 10 × UINT8 (1 byte) + } + + [Fact] + public void Allocate_ArrayStore_RegistersArrayWithTotalByteSize() + { + var prog = MakeProgram(funcs: [MakeFunc("main", [ + new ArrayStore("buf", new Variable("i"), new Variable("v"), DataType.UINT16, 4), + new Return(new NoneVal()) + ])]); + + var allocator = new StackAllocator(); + allocator.Allocate(prog); + + allocator.VariableSizes["buf"].Should().Be(8); // 4 × UINT16 (2 bytes) + } +} diff --git a/tests/csharp/Backend/License/LicenseResultTests.cs b/tests/csharp/Backend/License/LicenseResultTests.cs new file mode 100644 index 0000000..a20f26e --- /dev/null +++ b/tests/csharp/Backend/License/LicenseResultTests.cs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// PyMCU Backend SDK — Unit tests for LicenseResult factory methods. + +using FluentAssertions; +using PyMCU.Backend.License; +using Xunit; + +namespace PyMCU.Backend.SDK.Tests.Backend.License; + +public class LicenseResultTests +{ + [Fact] + public void Free_ReturnsValidStatus_WithFreeMessage() + { + var result = LicenseResult.Free(); + + result.Status.Should().Be(LicenseStatus.Valid); + result.Message.Should().Be("free"); + result.Email.Should().BeNull(); + result.ExpiryDate.Should().BeNull(); + } + + [Fact] + public void Ok_ReturnsValidStatus_WithEmailAndExpiry() + { + var expiry = new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var result = LicenseResult.Ok("dev@example.com", expiry); + + result.Status.Should().Be(LicenseStatus.Valid); + result.Email.Should().Be("dev@example.com"); + result.ExpiryDate.Should().Be(expiry); + } + + [Fact] + public void NotFound_ReturnsMissingStatus() + { + var result = LicenseResult.NotFound("avr"); + + result.Status.Should().Be(LicenseStatus.Missing); + result.Message.Should().Contain("avr"); + result.Message.Should().Contain("PYMCU_LICENSE_KEY"); + } + + [Fact] + public void Expired_ReturnsExpiredStatus_WithEmailAndDate() + { + var expiry = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc); + var result = LicenseResult.Expired("user@example.com", expiry); + + result.Status.Should().Be(LicenseStatus.Expired); + result.Email.Should().Be("user@example.com"); + result.ExpiryDate.Should().Be(expiry); + result.Message.Should().Contain("2024-06-01"); + } + + [Fact] + public void WrongFamily_ReturnsInvalidTargetStatus() + { + var result = LicenseResult.WrongFamily("pic14"); + + result.Status.Should().Be(LicenseStatus.InvalidTarget); + result.Message.Should().Contain("pic14"); + } +} diff --git a/tests/csharp/Backend/License/LicenseValidatorTests.cs b/tests/csharp/Backend/License/LicenseValidatorTests.cs new file mode 100644 index 0000000..0dd759b --- /dev/null +++ b/tests/csharp/Backend/License/LicenseValidatorTests.cs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +// PyMCU Backend SDK — Unit tests for LicenseValidator. + +using FluentAssertions; +using PyMCU.Backend.License; +using Xunit; + +namespace PyMCU.Backend.SDK.Tests.Backend.License; + +/// +/// Tests for LicenseValidator key resolution and JWT parsing. +/// +/// Because the SDK ships with the placeholder public key +/// ("REPLACE_WITH_REAL_RSA2048_PUBLIC_KEY_BASE64_HERE"), signature +/// verification is intentionally skipped in dev builds. These tests +/// therefore exercise key resolution, expiry, family matching, and +/// structural parsing — not the actual RSA signature. +/// +public class LicenseValidatorResolveKeyTests : IDisposable +{ + private readonly string _origEnv; + private readonly string _tmpHome; + + public LicenseValidatorResolveKeyTests() + { + _origEnv = Environment.GetEnvironmentVariable("PYMCU_LICENSE_KEY") ?? ""; + Environment.SetEnvironmentVariable("PYMCU_LICENSE_KEY", null); + _tmpHome = Path.Combine(Path.GetTempPath(), $"pymcu_test_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tmpHome); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("PYMCU_LICENSE_KEY", + string.IsNullOrEmpty(_origEnv) ? null : _origEnv); + if (Directory.Exists(_tmpHome)) + Directory.Delete(_tmpHome, recursive: true); + } + + [Fact] + public void ResolveKey_ExplicitKey_TakesPriority() + { + Environment.SetEnvironmentVariable("PYMCU_LICENSE_KEY", "env-key"); + LicenseValidator.ResolveKey("explicit-key").Should().Be("explicit-key"); + } + + [Fact] + public void ResolveKey_EnvVar_ResolvedWhenNoExplicit() + { + Environment.SetEnvironmentVariable("PYMCU_LICENSE_KEY", "env-key"); + LicenseValidator.ResolveKey().Should().Be("env-key"); + } + + [Fact] + public void ResolveKey_NoSources_ReturnsNull() + { + // Ensure env var is cleared and no home file exists for this test + Environment.SetEnvironmentVariable("PYMCU_LICENSE_KEY", null); + // Note: we can't easily fake the home dir in .NET without reflection, + // so we only verify when no env is set and ResolveKey returns null or a + // value from the home file (which may legitimately exist in the environment). + var result = LicenseValidator.ResolveKey(); + // Accept null or a non-empty string (in case a real license.key exists on the CI runner) + if (result != null) result.Should().NotBeEmpty(); + } + + [Fact] + public void ResolveKey_WhitespaceExplicit_TreatedAsNotSet() + { + LicenseValidator.ResolveKey(" ").Should().BeNull(); + } +} + +public class LicenseValidatorValidateTests +{ + // ── Pre-built test JWTs (placeholder sig — sig check is skipped in dev mode) ── + // + // Generated from: + // header = base64url({"alg":"RS256","typ":"JWT"}) + // payload = base64url({"sub":"...","iat":...,"exp":...,"backends":[...]}) + // jwt = header + "." + payload + ".ZmFrZXNpZ25hdHVyZQ" (fake sig bytes) + // + // These JWTs are intentionally unsigned dev tokens used only in tests. + + // Expires 2099 — "avr" backend + private const string ValidAvrJwt = + "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9" + + ".eyJzdWIiOiAidXNlckBleGFtcGxlLmNvbSIsICJpYXQiOiAxNzAwMDAwMDAwLCAiZXhwIjogNDEwMjQ0NDgwMCwgImJhY2tlbmRzIjogWyJhdnIiXX0" + + ".ZmFrZXNpZ25hdHVyZQ"; + + // Expired in 2001 — "avr" backend + private const string ExpiredJwt = + "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9" + + ".eyJzdWIiOiAidXNlckBleGFtcGxlLmNvbSIsICJpYXQiOiAxNzAwMDAwMDAwLCAiZXhwIjogMTAwMDAwMDAwMCwgImJhY2tlbmRzIjogWyJhdnIiXX0" + + ".ZmFrZXNpZ25hdHVyZQ"; + + // Expires 2099, "avr" only — queried for "pic14" + private const string WrongFamilyJwt = + "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9" + + ".eyJzdWIiOiAidXNlckBleGFtcGxlLmNvbSIsICJpYXQiOiAxNzAwMDAwMDAwLCAiZXhwIjogNDEwMjQ0NDgwMCwgImJhY2tlbmRzIjogWyJhdnIiXX0" + + ".ZmFrZXNpZ25hdHVyZQ"; + + // Expires 2099, no "backends" field — covers any family + private const string AllFamiliesJwt = + "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCJ9" + + ".eyJzdWIiOiAidXNlckBleGFtcGxlLmNvbSIsICJpYXQiOiAxNzAwMDAwMDAwLCAiZXhwIjogNDEwMjQ0NDgwMH0" + + ".ZmFrZXNpZ25hdHVyZQ"; + + [Fact] + public void Validate_NoKey_ReturnsMissing() + { + // Pass an explicit null so it doesn't read from env or file + var result = LicenseValidator.Validate("avr", ""); + result.Status.Should().Be(LicenseStatus.Missing); + } + + [Fact] + public void Validate_MalformedKey_ReturnsMalformed() + { + var result = LicenseValidator.Validate("avr", "not.a.jwt.with.too.many.dots.here.x"); + result.Status.Should().Be(LicenseStatus.Malformed); + } + + [Fact] + public void Validate_ValidAvrKey_CorrectFamily_ReturnsValid() + { + var result = LicenseValidator.Validate("avr", ValidAvrJwt); + result.Status.Should().Be(LicenseStatus.Valid); + result.Email.Should().Be("user@example.com"); + } + + [Fact] + public void Validate_ExpiredKey_ReturnsExpired() + { + var result = LicenseValidator.Validate("avr", ExpiredJwt); + result.Status.Should().Be(LicenseStatus.Expired); + result.Email.Should().Be("user@example.com"); + result.ExpiryDate.Should().BeBefore(DateTime.UtcNow); + } + + [Fact] + public void Validate_WrongFamily_ReturnsInvalidTarget() + { + var result = LicenseValidator.Validate("pic14", WrongFamilyJwt); + result.Status.Should().Be(LicenseStatus.InvalidTarget); + } + + [Fact] + public void Validate_AllFamilies_AllowsAnyFamily() + { + // No "backends" array in payload → all families accepted + var result = LicenseValidator.Validate("riscv", AllFamiliesJwt); + result.Status.Should().Be(LicenseStatus.Valid); + } + + [Fact] + public void Free_AlwaysReturnsValid() + { + LicenseValidator.Free().Status.Should().Be(LicenseStatus.Valid); + } + + [Fact] + public void Validate_CaseInsensitiveFamily_Match() + { + // "avr" in JWT, "AVR" queried + var result = LicenseValidator.Validate("AVR", ValidAvrJwt); + result.Status.Should().Be(LicenseStatus.Valid); + } +} diff --git a/tests/csharp/Backend/Serialization/IrSerializerTests.cs b/tests/csharp/Backend/Serialization/IrSerializerTests.cs new file mode 100644 index 0000000..cfa7a39 --- /dev/null +++ b/tests/csharp/Backend/Serialization/IrSerializerTests.cs @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT +// PyMCU Backend SDK — Unit tests for IrSerializer (JSON round-trip). + +using FluentAssertions; +using PyMCU.Backend.Serialization; +using PyMCU.IR; +using Xunit; + +namespace PyMCU.Backend.SDK.Tests.Backend.Serialization; + +public class IrSerializerTests : IDisposable +{ + private readonly string _tmpDir = Path.Combine(Path.GetTempPath(), $"irser_{Guid.NewGuid():N}"); + + public IrSerializerTests() => Directory.CreateDirectory(_tmpDir); + + public void Dispose() + { + if (Directory.Exists(_tmpDir)) + Directory.Delete(_tmpDir, recursive: true); + } + + private string TempFile(string name = "test.mir") => Path.Combine(_tmpDir, name); + + private static ProgramIR RoundTrip(ProgramIR prog) + { + var tmp = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.mir"); + try + { + IrSerializer.Serialize(prog, tmp); + return IrSerializer.Deserialize(tmp); + } + finally + { + if (File.Exists(tmp)) File.Delete(tmp); + } + } + + // ── Empty program round-trip ─────────────────────────────────────────── + + [Fact] + public void Serialize_EmptyProgram_RoundTrips() + { + var prog = new ProgramIR(); + var rt = RoundTrip(prog); + + rt.Globals.Should().BeEmpty(); + rt.Functions.Should().BeEmpty(); + rt.ExternSymbols.Should().BeEmpty(); + } + + // ── Globals ──────────────────────────────────────────────────────────── + + [Fact] + public void Serialize_Globals_RoundTrip() + { + var prog = new ProgramIR(); + prog.Globals.Add(new Variable("led", DataType.UINT8)); + prog.Globals.Add(new Variable("counter", DataType.UINT16)); + + var rt = RoundTrip(prog); + + rt.Globals.Should().HaveCount(2); + rt.Globals[0].Name.Should().Be("led"); + rt.Globals[0].Type.Should().Be(DataType.UINT8); + rt.Globals[1].Name.Should().Be("counter"); + rt.Globals[1].Type.Should().Be(DataType.UINT16); + } + + // ── ExternSymbols ───────────────────────────────────────────────────── + + [Fact] + public void Serialize_ExternSymbols_RoundTrip() + { + var prog = new ProgramIR(); + prog.ExternSymbols.AddRange(["uart_write", "delay_ms"]); + + var rt = RoundTrip(prog); + + rt.ExternSymbols.Should().BeEquivalentTo(["uart_write", "delay_ms"]); + } + + // ── Function metadata ───────────────────────────────────────────────── + + [Fact] + public void Serialize_FunctionMetadata_RoundTrips() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "isr_tim0", + ReturnType = DataType.VOID, + IsInterrupt = true, + IsInline = false, + InterruptVector = 2, + Params = ["arg1"] + }); + + var rt = RoundTrip(prog); + var f = rt.Functions[0]; + + f.Name.Should().Be("isr_tim0"); + f.ReturnType.Should().Be(DataType.VOID); + f.IsInterrupt.Should().BeTrue(); + f.InterruptVector.Should().Be(2); + f.Params.Should().ContainSingle("arg1"); + } + + // ── Instruction type round-trips ─────────────────────────────────────── + + [Fact] + public void Serialize_ReturnWithConstant_RoundTrips() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = [new Return(new Constant(42))] + }); + + var rt = RoundTrip(prog); + var ret = rt.Functions[0].Body.OfType().First(); + + ret.Value.Should().BeOfType().Which.Value.Should().Be(42); + } + + [Fact] + public void Serialize_CopyInstruction_RoundTrips() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = [new Copy(new Constant(1), new Variable("x", DataType.UINT8))] + }); + + var rt = RoundTrip(prog); + var copy = rt.Functions[0].Body.OfType().First(); + + copy.Src.Should().BeOfType().Which.Value.Should().Be(1); + copy.Dst.Should().BeOfType().Which.Name.Should().Be("x"); + } + + [Fact] + public void Serialize_BinaryInstruction_RoundTrips() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = [new Binary(BinaryOp.Add, new Variable("a"), new Variable("b"), new Temporary("t1"))] + }); + + var rt = RoundTrip(prog); + var bin = rt.Functions[0].Body.OfType().First(); + + bin.Op.Should().Be(BinaryOp.Add); + bin.Src1.Should().BeOfType().Which.Name.Should().Be("a"); + bin.Src2.Should().BeOfType().Which.Name.Should().Be("b"); + bin.Dst.Should().BeOfType().Which.Name.Should().Be("t1"); + } + + [Fact] + public void Serialize_UnaryInstruction_RoundTrips() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = [new Unary(UnaryOp.Neg, new Constant(5), new Temporary("t1"))] + }); + + var rt = RoundTrip(prog); + var u = rt.Functions[0].Body.OfType().First(); + + u.Op.Should().Be(UnaryOp.Neg); + u.Src.Should().BeOfType().Which.Value.Should().Be(5); + } + + [Fact] + public void Serialize_BitInstructions_RoundTrip() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = + [ + new BitSet(new Variable("port"), 3), + new BitClear(new Variable("port"), 5), + new BitCheck(new Variable("port"), 2, new Temporary("t1")), + new BitWrite(new Variable("port"), 4, new Variable("val")), + new Return(new NoneVal()) + ] + }); + + var rt = RoundTrip(prog); + var body = rt.Functions[0].Body; + + body.OfType().First().Bit.Should().Be(3); + body.OfType().First().Bit.Should().Be(5); + body.OfType().First().Bit.Should().Be(2); + body.OfType().First().Bit.Should().Be(4); + } + + [Fact] + public void Serialize_JumpInstructions_RoundTrip() + { + var prog = new ProgramIR(); + prog.Functions.Add(new Function + { + Name = "main", + Body = + [ + new JumpIfZero(new Variable("cond"), "end"), + new Jump("end"), + new Label("end"), + new Return(new NoneVal()) + ] + }); + + var rt = RoundTrip(prog); + var body = rt.Functions[0].Body; + + body.OfType().First().Target.Should().Be("end"); + body.OfType().First().Target.Should().Be("end"); + body.OfType