From 7a2c55cc517e9b881c3b8c53f2c9949d2a4bb48b Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Wed, 20 May 2026 17:53:12 -0400 Subject: [PATCH] testing: Implement Macros For Cli Testing Signed-off-by: Will Hawkins --- Sources/Macros/Macros.swift | 86 ++++++++++++++++++++++- Tests/Support.swift | 104 ++++++++++++++++++++++++++++ Tests/p4rseTests/CliTests/Cli.swift | 26 +++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 Tests/Support.swift create mode 100644 Tests/p4rseTests/CliTests/Cli.swift diff --git a/Sources/Macros/Macros.swift b/Sources/Macros/Macros.swift index 2e2f81f..c85a7d5 100644 --- a/Sources/Macros/Macros.swift +++ b/Sources/Macros/Macros.swift @@ -289,11 +289,95 @@ public struct MustOr: CodeItemMacro { } } +public struct CliTestDeclarationMacro: PeerMacro, Sendable { + + private static func doc_shrink(_ from: String) -> String { + return from.replacing(Regex(#/^.*\/\/\/[\s]+/#), with: "") + } + + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + + let cli_test_expected_output = node.leadingTrivia.filter({ $0.isComment }).map({ + doc_shrink("\($0)") + }).joined(separator: "\\n") + + let cli_test_driver_thunk_name = context.makeUniqueName( + declaration.cast(FunctionDeclSyntax.self).name.text + "_thunk_") + let cli_test_driver_thunk: DeclSyntax = """ + @Sendable private func \(cli_test_driver_thunk_name)() async throws { + let expected = "\(raw: cli_test_expected_output)" + + _ = unsafe try await Testing.__requiringUnsafe( + Testing.__requiringTry( + Testing.__requiringAwait(swiftCliTestRunner(\(declaration.cast(FunctionDeclSyntax.self).name), expected)))) + } + """ + + let cli_test_driver_generator_name = context.makeUniqueName( + declaration.cast(FunctionDeclSyntax.self).name.text + "_generator_") + let cli_test_driver_generator: DeclSyntax = """ + @Sendable private func \(cli_test_driver_generator_name)() async -> Testing.Test { + return .__function( + named: "xxxxxxxx()", + in: nil, + xcTestCompatibleSelector: Testing.__xcTestCompatibleSelector("xxxxxx:"), + traits: [], + sourceLocation: Testing.SourceLocation( + fileID: "Tests/CliTests/Cli.swift", + filePath: "/Users/hawkinsw/code/p4ce/Tests/p4rseTests/CliTests/Cli.swift", line: 359, + column: 2), + parameters: [], + testFunction: \(cli_test_driver_thunk_name) + ) + } + """ + + let cli_test_driver_accessor_name = context.makeUniqueName( + declaration.cast(FunctionDeclSyntax.self).name.text + "_accessor_") + let cli_test_driver_accessor: DeclSyntax = """ + private nonisolated let \(cli_test_driver_accessor_name): Accessor = { outValue, type, _, _ in + + Testing.Test.__store(\(cli_test_driver_generator_name), into: outValue, asTypeAt: type) + } + """ + + let cli_test_driver_content_record_name = context.makeUniqueName( + declaration.cast(FunctionDeclSyntax.self).name.text + "_cr_") + let cli_test_driver_cr: DeclSyntax = """ + private nonisolated let \(cli_test_driver_content_record_name): TestContentRecord = ( + 0x7465_7374, /* indicate a test */ + 0, + unsafe \(cli_test_driver_accessor_name), + 0, + 0 + ) + """ + + let cli_test_driver_content_container_name = context.makeUniqueName( + declaration.cast(FunctionDeclSyntax.self).name.text + "__🟡$_container_") + let cli_test_driver_container: DeclSyntax = """ + struct \(cli_test_driver_content_container_name): Testing.__TestContentRecordContainer { + nonisolated static var __testContentRecord: TestContentRecord { + unsafe \(cli_test_driver_content_record_name) + } + } + """ + return [ + cli_test_driver_thunk, cli_test_driver_generator, cli_test_driver_accessor, + cli_test_driver_cr, cli_test_driver_container, + ] + } +} + @main struct P4Macros: CompilerPlugin { var providingMacros: [Macro.Type] = [ RequireResult.self, RequireErrorResult.self, UseOkResult.self, UseErrorResult.self, RequireNodeType.self, SkipUnlessNodeType.self, SkipUnlessNodesTypes.self, RequireNodesType.self, - MustOr.self, + MustOr.self, CliTestDeclarationMacro.self, ] } diff --git a/Tests/Support.swift b/Tests/Support.swift new file mode 100644 index 0000000..a2f827c --- /dev/null +++ b/Tests/Support.swift @@ -0,0 +1,104 @@ +// p4rse, Copyright 2026, Will Hawkins +// +// This file is part of p4rse. +// +// This file is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import Foundation +import Testing + +typealias Accessor = + @convention(c) ( + _ outValue: UnsafeMutableRawPointer, + _ type: UnsafeRawPointer, + _ hint: UnsafeRawPointer?, + _ reserved: UInt + ) -> CBool + +typealias TestContentRecord = ( + kind: UInt32, + reserved1: UInt32, + accessor: Accessor?, + context: UInt, + reserved2: UInt +) + +func findPathEnv() -> [URL]? { + for (key, value) in ProcessInfo().environment { + if key == "PATH" { + return value.split(separator: ":").map { URL(filePath: "\($0)") } + } + } + + return .none +} + +func findInPath(_ what: String) -> URL? { + guard let bin_paths = findPathEnv() else { + return .none + } + + let fm = FileManager() + for bin_path in bin_paths { + let sought = bin_path.appending(path: what) + if fm.fileExists(atPath: sought.path) { + return sought + } + } + + return .none +} + +func swiftPath() -> URL? { + return findInPath("swift") +} + +func swiftRun(withArgs args: [String]) throws -> String? { + let path = swiftPath()! + let child = Process() + let so = Pipe() + let se = Pipe() + + child.standardOutput = so + child.executableURL = path + child.arguments = + [ + "run", + "--ignore-lock" /* --ignore-lock needs to be early because it is a "location option". */, + "--skip-build", + ] + args + + try! child.run() + + let output = + switch child.standardOutput { + case let so as Pipe: + String(data: try! so.fileHandleForReading.readToEnd()!, encoding: String.Encoding.ascii) + default: Optional.none + } + return output +} + +func swiftCliTestRunner(_ arg_gen: () -> [String], _ expected: String) async throws { + let args = arg_gen() + + let run_output = try! swiftRun(withArgs: args) + + #expect(run_output == expected) +} + + + +@attached(peer) public macro CliTest() = + #externalMacro(module: "Macros", type: "CliTestDeclarationMacro") \ No newline at end of file diff --git a/Tests/p4rseTests/CliTests/Cli.swift b/Tests/p4rseTests/CliTests/Cli.swift new file mode 100644 index 0000000..3ac0b95 --- /dev/null +++ b/Tests/p4rseTests/CliTests/Cli.swift @@ -0,0 +1,26 @@ +// p4rse, Copyright 2026, Will Hawkins +// +// This file is part of p4rse. +// +// This file is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import Foundation +import Testing + +/// Success +/// +@CliTest() +func simple_cli_test() -> [String] { + return ["p4ce", "--plain", "compile", "simple.p4", "-I", "TestData/Sources/"] +} \ No newline at end of file