diff --git a/Changelog.md b/Changelog.md
index 3541bca..6bd5e22 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed Swift version requirement to 5.3.
#29 by @mattt.
+- Changed expression syntax.
+ #30 by @mattt.
+
+### Fixed
+
+- Fixed a bug that caused DocTest annotations to be missed.
+ #30 by @mattt.
## [0.1.0] - 2020-05-04
diff --git a/Package.swift b/Package.swift
index 5a5c24c..84dda23 100644
--- a/Package.swift
+++ b/Package.swift
@@ -34,7 +34,6 @@ let package = Package(
name: "swift-doctest",
dependencies: [
.target(name: "DocTest"),
- .product(name: "StringLocationConverter", package: "StringLocationConverter"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
]),
@@ -42,6 +41,7 @@ let package = Package(
name: "DocTest",
dependencies: [
.product(name: "SwiftSyntax", package: "SwiftSyntax"),
+ .product(name: "StringLocationConverter", package: "StringLocationConverter"),
.product(name: "TAP", package: "TAP"),
]),
.testTarget(
diff --git a/README.md b/README.md
index c3d9c41..7b591da 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# DocTest
+![CI][ci badge]
+
**DocTest** is an experimental tool
for testing Swift example code in documentation.
@@ -73,7 +75,7 @@ OVERVIEW: A utility for syntax testing documentation in Swift code.
USAGE: swift-doctest [--swift-launch-path ] [--package] [--assumed-filename ]
ARGUMENTS:
- Swift code or a path to a Swift file
+ Swift code or a path to a Swift file
OPTIONS:
--swift-launch-path
@@ -130,22 +132,20 @@ This tells the documentation test runner to evaluate the code sample.
```
By adding an annotation in the format
-`=> (Type) = (Value)`,
+`=> <#Value#>`,
we can test the expected type and value
of the expression.
```diff
- add(1 1) // 3.0
-+ add(1 1) // => Double = 3.0
++ add(1 1) // => 3.0
```
Run the `swift-doctest` command
-from the root directory of the Swift package,
-specifying the `--package` flag
-(to invoke the Swift REPL via the Swift Package Manager)
+from the root directory of the Swift package
and passing the path to the file containing the `add(_:_:)` function.
This will scan for all of code blocks annotated with
-```swift doctest
+```swift doctest,
run them through the Swift REPL,
and test the output with any annotated expectations.
@@ -153,7 +153,7 @@ and test the output with any annotated expectations.
$ swift doctest --package path/to/file.swift
TAP version 13
1..1
-not ok 1 - `add(1 1)` did not produce `Double = 3.0`
+not ok 1 - `add(1 1)` did not produce `3.0`
---
column: 1
file: path/to/file.swift.md
@@ -172,7 +172,7 @@ we update the documentation to fix the example.
Returns the sum of two integers.
```swift doctest
- add(1, 1) // => Int = 2
+ add(1, 1) // => 2
```
*/
func add(_ a: Int, _ b: Int) -> Int { ... }
@@ -185,7 +185,7 @@ the tests now pass as expected.
$ swift doctest --package path/to/file.swift
TAP version 13
1..1
-ok 1 - `add(1, 1)` produces `Int = 2`
+ok 1 - `add(1, 1)` produces `2`
---
column: 1
file: path/to/file.swift.md
@@ -210,3 +210,5 @@ Mattt ([@mattt](https://twitter.com/mattt))
[seccomp]: https://docs.docker.com/engine/security/seccomp/
[apparmor]: https://docs.docker.com/engine/security/apparmor/
+
+[ci badge]: https://github.com/SwiftDocOrg/DocTest/workflows/CI/badge.svg
diff --git a/Sources/DocTest/Expectation.swift b/Sources/DocTest/Expectation.swift
index 6ef9200..31656ad 100644
--- a/Sources/DocTest/Expectation.swift
+++ b/Sources/DocTest/Expectation.swift
@@ -1,19 +1,42 @@
import Foundation
public enum Expectation: Hashable {
- case value(String)
case error
+ case type(String)
+ case value(String)
+ case match(String)
public init?(_ string: String?) {
- guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil }
- if string.starts(with: "=>"),
- let index = string.firstIndex(where: { $0.isWhitespace })
- {
- self = .value(string.suffix(from: index).trimmingCharacters(in: .whitespaces))
- } else if string.starts(with: "!!") {
+ guard let string = string?.trimmed,
+ let index = string.index(string.startIndex, offsetBy: 2, limitedBy: string.endIndex)
+ else { return nil }
+
+ switch string.prefix(upTo: index) {
+ case "!!":
self = .error
- } else {
+ case "->":
+ self = .type(string.suffix(from: index).trimmed)
+ case "=>":
+ self = .value(string.suffix(from: index).trimmed)
+ case "~>":
+ self = .match(string.suffix(from: index).trimmed)
+ default:
return nil
}
}
+
+ public func evaluate(_ output: String) -> Bool {
+ let output = output.trimmed
+
+ switch self {
+ case .error:
+ return output.hasPrefix("error:")
+ case .type(let type):
+ return output.hasPrefix("\(type) =")
+ case .value(let value):
+ return output.hasSuffix("= \(value)")
+ case .match(let pattern):
+ return output.range(of: pattern, options: .regularExpression) != nil
+ }
+ }
}
diff --git a/Sources/DocTest/Extensions/StringProtocol+Extensions.swift b/Sources/DocTest/Extensions/StringProtocol+Extensions.swift
new file mode 100644
index 0000000..285ddad
--- /dev/null
+++ b/Sources/DocTest/Extensions/StringProtocol+Extensions.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension StringProtocol {
+ var trimmed: String {
+ trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+}
diff --git a/Sources/DocTest/REPL.swift b/Sources/DocTest/REPL.swift
index df2f876..9782aea 100644
--- a/Sources/DocTest/REPL.swift
+++ b/Sources/DocTest/REPL.swift
@@ -17,9 +17,9 @@ public class REPL {
public var description: String
public init?(_ description: String) {
- let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmedDescription.isEmpty else { return nil }
- self.description = trimmedDescription
+ let description = description.trimmed
+ guard !description.isEmpty else { return nil }
+ self.description = description
}
}
@@ -41,7 +41,7 @@ public class REPL {
public var evaluationHandler: ((Statement, Result) -> Void)?
- public init(configuration: Configuration) {
+ init(configuration: Configuration) {
process = Process()
if #available(OSX 10.13, *) {
@@ -108,7 +108,7 @@ public class REPL {
}
}
- public func evaluate(_ statement: Statement) {
+ func evaluate(_ statement: Statement) {
if !process.isRunning {
if #available(OSX 10.13, *) {
try! process.run()
@@ -133,12 +133,11 @@ public class REPL {
outputPipe.fileHandleForReading.readabilityHandler = nil
}
- public func waitUntilExit() {
+ func waitUntilExit() {
process.waitUntilExit()
}
- public func close() {
-
+ func close() {
if #available(OSX 10.15, *) {
try! self.inputPipe.fileHandleForWriting.close()
}
diff --git a/Sources/DocTest/Runner.swift b/Sources/DocTest/Runner.swift
index aa2df3d..4c70507 100644
--- a/Sources/DocTest/Runner.swift
+++ b/Sources/DocTest/Runner.swift
@@ -37,14 +37,14 @@ public final class Runner {
let repl = REPL(configuration: configuration)
- repl.evaluationHandler = { (statement, result) in
- tests.append(contentsOf: statement.tests(with: result))
- }
-
for statement in statements {
repl.evaluate(statement)
}
+ repl.evaluationHandler = { (statement, result) in
+ tests.append(contentsOf: statement.tests(with: result))
+ }
+
repl.close()
repl.waitUntilExit()
diff --git a/Sources/DocTest/Scanner.swift b/Sources/DocTest/Scanner.swift
new file mode 100644
index 0000000..7cf227c
--- /dev/null
+++ b/Sources/DocTest/Scanner.swift
@@ -0,0 +1,48 @@
+import Foundation
+import StringLocationConverter
+
+public class Scanner {
+ public typealias Match = (line: Int, column: Int, content: String)
+
+ private var regularExpression: NSRegularExpression
+
+ public init() throws {
+ let pattern = #"""
+ ^
+ \h* \`{3} \h* swift \h+ doctest \h* \n
+ (.+)\n
+ \h* \`{3} \h*
+ $
+ """#
+ self.regularExpression = try NSRegularExpression(pattern: pattern,
+ options: [
+ .allowCommentsAndWhitespace,
+ .anchorsMatchLines,
+ .caseInsensitive,
+ .dotMatchesLineSeparators
+ ])
+ }
+
+ public func matches(in source: String) -> [Match] {
+ let range = NSRange(source.startIndex..) -> [Test] {
+ func tests(with result: Result) -> [Test] {
var metadata: [String: Any] = [
"file": self.sourceLocation.file as Any?,
"line": self.sourceLocation.line as Any?,
@@ -31,16 +31,18 @@ public class Statement {
} else {
return expectations.map { expectation in
switch expectation {
- case .value(let expected):
+ case .error:
+ return test {
+ .success("- `\(self.code)` produced an error, as expected", directive: nil, metadata: metadata)
+ }
+ case .type(let expected),
+ .value(let expected),
+ .match(let expected):
metadata["expected"] = expected
return test {
.failure("- `\(self.code)` produced an error", directive: nil, metadata: metadata)
}
- case .error:
- return test {
- .success("- `\(self.code)` produced an error, as expected", directive: nil, metadata: metadata)
- }
}
}
}
@@ -49,10 +51,12 @@ public class Statement {
return expectations.map { expectation in
switch expectation {
- case .value(let expected):
+ case .type(let expected),
+ .value(let expected),
+ .match(let expected):
metadata["expected"] = expected
- if actual == expected {
+ if expectation.evaluate(actual) {
return test {
.success("- `\(self.code)` produces `\(actual)`", directive: nil, metadata: metadata)
}
diff --git a/Sources/swift-doctest/main.swift b/Sources/swift-doctest/main.swift
index f4e51d8..f7b64d7 100644
--- a/Sources/swift-doctest/main.swift
+++ b/Sources/swift-doctest/main.swift
@@ -66,8 +66,7 @@ struct SwiftDocTest: ParsableCommand {
logger.debug("Swift launch path: \(configuration.launchPath)")
logger.debug("Swift launch arguments: \(configuration.arguments)")
- let pattern = #"^\`{3}\s*swift\s+doctest\s*\n(.+)\n\`{3}$"#
- let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines, .dotMatchesLineSeparators])
+ let scanner = try Scanner()
let source: String
let assumedFileName: String
@@ -82,29 +81,24 @@ struct SwiftDocTest: ParsableCommand {
logger.trace("Scanning standard input for DocTest blocks")
}
- let converter = StringLocationConverter(for: source)
-
var reports: [Report] = []
let group = DispatchGroup()
- regex.enumerateMatches(in: source, options: [], range: NSRange(source.startIndex.. Int"), .type("Int"))
+ }
+
+ func testValueExpectation() throws {
+ XCTAssertEqual(Expectation("=> 2"), .value("2"))
+ }
+
+ func testMatchExpectation() throws {
+ XCTAssertEqual(Expectation(#"~> Int = \d+"#), .match(#"Int = \d+"#))
+ }
+
+ func testErrorExpectation() throws {
+ XCTAssertEqual(Expectation("!! error: division by zero"), .error)
+ }
+
+ func testInvalidExpectation() throws {
+ XCTAssertNil(Expectation("invalid"))
+ }
+}
diff --git a/Tests/DocTestTests/DocTestTests.swift b/Tests/DocTestTests/RunnerTests.swift
similarity index 50%
rename from Tests/DocTestTests/DocTestTests.swift
rename to Tests/DocTestTests/RunnerTests.swift
index 105931d..90d40a3 100644
--- a/Tests/DocTestTests/DocTestTests.swift
+++ b/Tests/DocTestTests/RunnerTests.swift
@@ -1,14 +1,14 @@
import XCTest
import DocTest
-final class DocTestTests: XCTestCase {
- func testRunner() throws {
+final class RunnerTests: XCTestCase {
+ func testExample() throws {
let source = #"""
- 1 + 1 // => Int = 2
- 1 + 1 // => String = "wat"
- 1 / 0 // !! Error
+ 1 + 2 // => 3
+ 1 + 2 // -> String
+ 1 / 0 // !! error: division by zero
invalid
- 1 + 1 // => Int = 2
+ 1 + 2 // ~> Int = \d
"""#
let expectation = XCTestExpectation()
@@ -20,11 +20,11 @@ final class DocTestTests: XCTestCase {
XCTFail("\(error)")
case .success(let report):
XCTAssertEqual(report.results.count, 5)
- XCTAssertTrue(try! report.results[0].get().ok) // 1 + 1 => 2
- XCTAssertFalse(try! report.results[1].get().ok) // 1 + 1 => "wat"
- XCTAssertTrue(try! report.results[2].get().ok) // 1 / 0 !! Error
- XCTAssertFalse(try! report.results[3].get().ok) // invalid
- XCTAssertTrue(try! report.results[4].get().ok) // 1 + 1 => 2
+ XCTAssertTrue(try! report.results[0].get().ok)
+ XCTAssertFalse(try! report.results[1].get().ok)
+ XCTAssertTrue(try! report.results[2].get().ok)
+ XCTAssertFalse(try! report.results[3].get().ok)
+ XCTAssertTrue(try! report.results[4].get().ok)
expectation.fulfill()
}
diff --git a/Tests/DocTestTests/ScannerTests.swift b/Tests/DocTestTests/ScannerTests.swift
new file mode 100644
index 0000000..5c5dd94
--- /dev/null
+++ b/Tests/DocTestTests/ScannerTests.swift
@@ -0,0 +1,36 @@
+import XCTest
+import DocTest
+
+final class ScannerTests: XCTestCase {
+ func testExample() throws {
+ let scanner = try Scanner()
+
+
+ let source = #"""
+ /**
+ Returns the sum of two integers.
+
+ ```swift doctest
+ add(1, 3) // => 2
+ ```
+ */
+ func add(_ a: Int, _ b: Int) -> Int {
+ return a + b
+ }
+
+ /**
+ Returns the product of two integers.
+ */
+ func multiply(_ a: Int, _ b: Int) -> Int {
+ return a * b
+ }
+ """#
+
+ let matches = scanner.matches(in: source)
+
+ XCTAssertEqual(matches.count, 1)
+ XCTAssertEqual(matches.first?.line, 5)
+ XCTAssertEqual(matches.first?.column, 1)
+ XCTAssertEqual(matches.first?.content, "add(1, 3) // => 2")
+ }
+}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
deleted file mode 100644
index 22c49c5..0000000
--- a/Tests/LinuxMain.swift
+++ /dev/null
@@ -1,7 +0,0 @@
-import XCTest
-
-import doctestTests
-
-var tests = [XCTestCaseEntry]()
-tests += doctestTests.allTests()
-XCTMain(tests)