Compare commits

...

5 Commits

Author SHA1 Message Date
Will Hawkins 8f9fbb86bf testing: Cli Tests Are Now Integrated
Continuous Integration / Grammar Tests (push) Successful in 37s
Continuous Integration / Library Format Tests (push) Failing after 1m27s
Continuous Integration / Library Tests (push) Failing after 4m14s
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 00:11:07 -04:00
Will Hawkins 16a798cc39 testing: Update to Latest ABI Protocol for Test Discovery
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-22 00:10:10 -04:00
Will Hawkins 017d5670c0 Make Formatter Happy
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-21 22:42:55 -04:00
Will Hawkins d60465e669 testing: Update CliTest Support.
And add tests for Cli preprocessing.

Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-21 22:41:00 -04:00
Will Hawkins b3ca30541a cli: Add a Preprocess Mode
Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
2026-05-21 22:40:14 -04:00
10 changed files with 176 additions and 77 deletions
-21
View File
@@ -56,25 +56,4 @@ jobs:
- run: tree-sitter generate
working-directory: ./tree-sitter-p4
- run: ./ci/format.sh
cli-tests:
name: Cli Tests
runs-on: ubuntu-build
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v4
- uses: tree-sitter/setup-action@v2
with:
tree-sitter-ref: "master"
- uses: swift-actions/setup-swift@v3
with:
swift-version: "main-snapshot"
skip-verify-signature: true
# Because our tree-sitter code is in a subdirectory,
# and working-directory does not apply to uses, we
# are forced to specify calls to the CI ourselves.
# See https://github.com/orgs/community/discussions/25742
- run: tree-sitter generate
working-directory: ./tree-sitter-p4
- run: swift build
- run: ./ci/tests/test.sh
+1 -1
View File
@@ -1 +1 @@
6.2.4
main-snapshot
+31 -1
View File
@@ -27,7 +27,7 @@ struct Cli: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "P4CE compiler, interpreter and debugger.",
subcommands: [Compile.self, CodeGen.self])
subcommands: [Preprocess.self, Compile.self, CodeGen.self])
}
struct CliOptions: ParsableArguments {
@@ -38,6 +38,36 @@ struct CliOptions: ParsableArguments {
var search: [String] = []
}
extension Cli {
struct Preprocess: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Compile P4CE code.")
@ParentCommand var parent: Cli
@OptionGroup var options: CliOptions
mutating func run() {
let sm = SourceManager(options.search.map { FilePath($0) })
let prep = SourceCodePreprocessor(sm)
let file = FilePath(options.path)
let formatter: any Formattable =
if parent.plain != 0 {
FormatterPlain()
} else {
FormatterAnsi()
}
let maybe_source = prep.preprocess(file)
guard case .Ok(let source) = maybe_source else {
print(ErrorWithLabel("Preprocessor Error", maybe_source.error()!).format(formatter))
return
}
print(source.getSource())
}
}
}
extension Cli {
struct Compile: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Compile P4CE code.")
+103 -37
View File
@@ -291,6 +291,77 @@ public struct MustOr: CodeItemMacro {
public struct CliTestDeclarationMacro: PeerMacro, Sendable {
// NOTE: Taken from swift-testing.
/// Get an expression initializing an instance of ``SourceLocation`` from an
/// arbitrary syntax node.
///
/// - Parameters:
/// - node: The syntax node for which an instance of ``SourceLocation`` is
/// needed.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: An expression value that initializes an instance of
/// ``SourceLocation`` for `node`.
static func createSourceLocationExpr(
of node: some SyntaxProtocol, context: some MacroExpansionContext
) -> ExprSyntax {
if node.isProtocol((any FreestandingMacroExpansionSyntax).self) {
// Freestanding macro expressions can just use __here()
// directly and do not need to talk to the macro context to get source
// location info.
return "Testing.SourceLocation.__here()"
}
// Get the equivalent source location in both `#fileID` and `#filePath` modes.
guard let fileIDSourceLoc: AbstractSourceLocation = context.location(of: node),
let filePathSourceLoc: AbstractSourceLocation = context.location(
of: node, at: .afterLeadingTrivia, filePathMode: .filePath)
else {
return "Testing.SourceLocation.__here()"
}
return
"Testing.SourceLocation(__uncheckedFileID: \(fileIDSourceLoc.file), filePath: \(filePathSourceLoc.file), line: \(fileIDSourceLoc.line), column: \(fileIDSourceLoc.column))"
}
/// Get an expression initializing an instance of `__SourceBounds` from two
/// arbitrary syntax nodesvalues.
///
/// - Parameters:
/// - lowerBoundNode: The syntax node representing the lower bound. The start
/// of this node (after leading trivia) is used.
/// - upperBoundNode: The syntax node representing the upper bound. The end of
/// this node (before trailing trivia) is used.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: An expression value that initializes an instance of
/// `__SourceBounds`.
///
/// The resulting source bounds instance represents (approximately):
///
/// ```swift
/// lowerBoundNode.positionAfterSkippingLeadingTrivia ..< upperBoundNode.endPositionBeforeTrailingTrivia
/// ```
static func createSourceBoundsExpr(
from lowerBoundNode: some SyntaxProtocol, to upperBoundNode: some SyntaxProtocol,
in context: some MacroExpansionContext
) -> ExprSyntax {
let lowerBoundExpr = createSourceLocationExpr(of: lowerBoundNode, context: context)
let upperBoundExpr: ExprSyntax =
if let upperBoundSourceLoc = context.location(
of: upperBoundNode, at: .beforeTrailingTrivia, filePathMode: .fileID)
{
"(\(upperBoundSourceLoc.line), \(upperBoundSourceLoc.column))"
} else {
"(.max, .max)"
}
return
"Testing.__SourceBounds(__uncheckedLowerBound: \(lowerBoundExpr), upperBound: \(upperBoundExpr))"
}
// NOTE: End of what was taken from swift-testing
private static func doc_shrink(_ from: String) -> String {
return from.replacing(Regex(#/^.*\/\/\/[\s]+/#), with: "")
}
@@ -301,74 +372,69 @@ public struct CliTestDeclarationMacro: PeerMacro, Sendable {
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let test_name = declaration.cast(FunctionDeclSyntax.self).name
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_name = context.makeUniqueName("_thunk_")
let (expected_decl, expected_label) =
if cli_test_expected_output.isEmpty {
("let expected = \"\(test_name)\"", "withExpectedPath:")
} else {
("let expected = \"\(cli_test_expected_output)\"", "withExpected:")
}
let cli_test_driver_thunk: DeclSyntax = """
@Sendable private func \(cli_test_driver_thunk_name)() async throws {
let expected = "\(raw: cli_test_expected_output)"
\(raw: expected_decl)
_ = unsafe try await Testing.__requiringUnsafe(
Testing.__requiringTry(
Testing.__requiringAwait(swiftCliTestRunner(\(declaration.cast(FunctionDeclSyntax.self).name), expected))))
Testing.__requiringAwait(swiftCliTestRunner(\(test_name), \(raw: expected_label) expected))))
}
"""
let cli_test_driver_generator_name = context.makeUniqueName(
declaration.cast(FunctionDeclSyntax.self).name.text + "_generator_")
let source_bounds = createSourceBoundsExpr(from: node, to: declaration, in: context)
let cli_test_driver_generator_name = context.makeUniqueName("_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:"),
named: "\(test_name)",
in: nil as Swift.Never.Type?,
xcTestCompatibleSelector: Testing.__xcTestCompatibleSelector("\(test_name)"),
traits: [],
sourceLocation: Testing.SourceLocation(
fileID: "Tests/CliTests/Cli.swift",
filePath: "/Users/hawkinsw/code/p4ce/Tests/p4rseTests/CliTests/Cli.swift", line: 359,
column: 2),
sourceBounds: \(source_bounds),
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
#if os(macOS)
let section = "__DATA_CONST,__swift5_tests"
#else
let section = "swift5_tests"
#endif
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_content_record_name = context.makeUniqueName("testContentRecord")
let cli_test_driver_cr: DeclSyntax = """
private nonisolated let \(cli_test_driver_content_record_name): TestContentRecord = (
@section("\(raw: section)")
@used
private nonisolated let \(cli_test_driver_content_record_name): Testing.__TestContentRecord = (
0x7465_7374, /* indicate a test */
0,
unsafe \(cli_test_driver_accessor_name),
{ outValue, type, _, _ in
Testing.Test.__store(\(cli_test_driver_generator_name), into: outValue, asTypeAt: type)
},
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,
cli_test_driver_thunk, cli_test_driver_generator, cli_test_driver_cr,
]
}
}
@@ -0,0 +1,8 @@
parser main_parser() {
state start {
transition select (false) {
true: reject;
false: reject;
};
}
};
@@ -0,0 +1 @@
Preprocessor Error: Could not open simple.p for preprocessing
+1
View File
@@ -0,0 +1 @@
Success
+15 -2
View File
@@ -16,8 +16,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import Foundation
import SystemPackage
import Testing
private let TestGoldenPath: FilePath.Component = FilePath.Component("TestData")
private let TestGoldenExtension: String = ".golden"
typealias Accessor =
@convention(c) (
_ outValue: UnsafeMutableRawPointer,
@@ -68,7 +72,6 @@ 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
@@ -90,7 +93,7 @@ func swiftRun(withArgs args: [String]) throws -> String? {
return output
}
func swiftCliTestRunner(_ arg_gen: () -> [String], _ expected: String) async throws {
func swiftCliTestRunner(_ arg_gen: () -> [String], withExpected expected: String) async throws {
let args = arg_gen()
let run_output = try! swiftRun(withArgs: args)
@@ -98,7 +101,17 @@ func swiftCliTestRunner(_ arg_gen: () -> [String], _ expected: String) async thr
#expect(run_output == expected)
}
func swiftCliTestRunner(_ arg_gen: () -> [String], withExpectedPath expected_path: String) async throws {
let args = arg_gen()
let run_output = try! swiftRun(withArgs: args)
let expected_source_absolute = FilePath(FileManager().currentDirectoryPath).appending(TestGoldenPath).appending(expected_path + TestGoldenExtension)
let expected_output = try! String(contentsOfFile: expected_source_absolute.string)
#expect(run_output == expected_output)
}
@attached(peer) public macro CliTest() =
#externalMacro(module: "Macros", type: "CliTestDeclarationMacro")
+15
View File
@@ -24,3 +24,18 @@ import Testing
func simple_cli_test() -> [String] {
return ["p4ce", "--plain", "compile", "simple.p4", "-I", "TestData/Sources/"]
}
@CliTest()
func simple_cli_testa() -> [String] {
return ["p4ce", "--plain", "compile", "simple.p4", "-I", "TestData/Sources/"]
}
@CliTest()
func simple_cli_preprocessor_test() -> [String] {
return ["p4ce", "--plain", "preprocess", "simple.p4", "-I", "TestData/Sources/"]
}
@CliTest()
func simple_cli_preprocessor_test_file_not_found() -> [String] {
return ["p4ce", "--plain", "preprocess", "simple.p", "-I", "TestData/Sources/"]
}
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
actual_output=`swift run p4ce --plain compile simple.p4 -I TestData/Sources/`
expected_output=`cat TestData/Cli/simple.golden`
if [ "${expected_output}" != "${actual_output}" ]; then
echo "Expected: ${expected_output}"
echo "Actual: ${actual_output}"
echo "Cli Tests: Error"
exit 1
fi
echo "Cli Tests: Success"
exit 0