Skip to content
This repository was archived by the owner on Sep 16, 2021. It is now read-only.

Commit 94d339c

Browse files
committed
Initial commit
0 parents  commit 94d339c

13 files changed

+642
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
.swiftpm

Package.resolved

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"object": {
3+
"pins": [
4+
{
5+
"package": "swift-argument-parser",
6+
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
7+
"state": {
8+
"branch": null,
9+
"revision": "9f04d1ff1afbccd02279338a2c91e5f27c45e93a",
10+
"version": "0.0.5"
11+
}
12+
},
13+
{
14+
"package": "SwiftSyntax",
15+
"repositoryURL": "https://github.com/apple/swift-syntax.git",
16+
"state": {
17+
"branch": "0.50200.0",
18+
"revision": "0688b9cfc4c3dd234e4f55f1f056b2affc849873",
19+
"version": null
20+
}
21+
},
22+
{
23+
"package": "TAP",
24+
"repositoryURL": "https://github.com/SwiftDocOrg/TAP.git",
25+
"state": {
26+
"branch": null,
27+
"revision": "9c8da52495ebaf3275c5fa03cb423d1272351ec2",
28+
"version": null
29+
}
30+
}
31+
]
32+
},
33+
"version": 1
34+
}

Package.swift

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// swift-tools-version:5.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "DocTest",
8+
platforms: [
9+
.macOS(.v10_10)
10+
],
11+
products: [
12+
.executable(name: "swift-doctest", targets: ["swift-doctest"])
13+
],
14+
dependencies: [
15+
// Dependencies declare other packages that this package depends on.
16+
.package(url: "https://github.com/apple/swift-syntax.git", .revision("0.50200.0")),
17+
.package(url: "https://github.com/SwiftDocOrg/TAP.git", .revision("9c8da52495ebaf3275c5fa03cb423d1272351ec2")),
18+
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.4")),
19+
],
20+
targets: [
21+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
22+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
23+
.target(
24+
name: "swift-doctest",
25+
dependencies: ["DocTest", "ArgumentParser"]),
26+
.target(
27+
name: "DocTest",
28+
dependencies: ["SwiftSyntax", "TAP"]),
29+
.testTarget(
30+
name: "DocTestTests",
31+
dependencies: ["DocTest"]),
32+
]
33+
)

README.md

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# DocTest
2+
3+
**DocTest** is an experimental tool
4+
for testing Swift example code in documentation.
5+
6+
**This is still a work-in-progress, and not yet ready for production**
7+
8+
> DocTest is inspired by
9+
> [Python's `doctest`](https://docs.python.org/3/library/doctest.html).
10+
11+
* * *
12+
13+
The hardest part of software documentation is writing it in the first place.
14+
But once you clear that initial hurdle,
15+
the challenge becomes keeping documentation correct and up-to-date.
16+
17+
There's no built-in feedback mechanism for documentation like there is for code.
18+
If you write invalid code,
19+
your compiler will tell you.
20+
If you write valid but incorrect code,
21+
your test suite will tell you.
22+
But if you write documentation with invalid or incorrect example code,
23+
nothing breaks (at least not immediately).
24+
25+
DocTest offers a way to annotate Swift code examples in documentation
26+
with expectations about its behavior that can be tested automatically,
27+
just like unit tests.
28+
29+
## Usage
30+
31+
```
32+
OVERVIEW: A utility for syntax testing documentation in Swift code.
33+
34+
USAGE: swift-doc-test <input> [--swift-launch-path <swift-launch-path>] [--package] [--assumed-filename <assumed-filename>]
35+
36+
ARGUMENTS:
37+
<input> Swift code or a path to a Swift file
38+
39+
OPTIONS:
40+
--swift-launch-path <swift-launch-path>
41+
The path to the swift executable. (default:
42+
/usr/bin/swift)
43+
-p, --package Whether to run the REPL through Swift Package Manager
44+
(`swift run --repl`).
45+
--assumed-filename <assumed-filename>
46+
The assumed filename to use for reporting when
47+
parsing from standard input. (default: Untitled.swift)
48+
-h, --help Show help information.
49+
50+
```
51+
52+
## How It Works
53+
54+
DocTest launches and interacts with
55+
the Swift <abbr title="Read-Eval-Print-Loop">REPL</abbr>,
56+
passing each code statement through standard input
57+
and reading its result through standard output and/or standard error.
58+
59+
Consider the following function declaration
60+
within a Swift package:
61+
62+
~~~swift
63+
/**
64+
Returns the sum of two integers.
65+
66+
```swift
67+
add(1 1) // 3.0
68+
```
69+
*/
70+
func add(_ a: Int, _ b: Int) -> Int { ... }
71+
~~~
72+
73+
There are three problems with the example code
74+
provided in the documentation for `add(_:_:)`:
75+
76+
1. It doesn't compile
77+
(missing comma separator between arguments)
78+
2. It suggests an incorrect result
79+
(one plus one equals two, not three)
80+
3. It suggests an incorrect type of result
81+
(the sum of two integers is an integer,
82+
which isn't expressible by a floating-point literal)
83+
84+
We can use DocTest to identify these problems automatically
85+
by adding `"doctest"` to the start of the fenced code block.
86+
This tells the documentation test runner to evaluate the code sample.
87+
88+
~~~swift
89+
/**
90+
Returns the sum of two integers.
91+
92+
```swift doctest
93+
add(1 1) // Double = 3.0
94+
```
95+
*/
96+
func add(_ a: Int, _ b: Int) -> Int { ... }
97+
~~~
98+
99+
Run the `swift-doctest` command
100+
from the root directory of the Swift package,
101+
specifying the `--package` flag
102+
(to invoke the Swift REPL via the Swift Package Manager)
103+
and passing the path to the file containing the `add(_:_:)` function.
104+
This will scan for all of code blocks annotated with
105+
<code>```swift doctest</code>
106+
run them through the Swift REPL,
107+
and test the output with any annotated expectations.
108+
109+
```terminal
110+
$ swift doctest --package path/to/file.swift
111+
TAP version 13
112+
1..1
113+
not ok 1 - `add(1 1)` did not produce `Double 3.0`
114+
---
115+
column: 1
116+
file: path/to/file.swift.md
117+
line: 1
118+
...
119+
120+
```
121+
122+
> Test results are reported in [TAP format](https://testanything.org).
123+
124+
## License
125+
126+
MIT
127+
128+
## Contact
129+
130+
Mattt ([@mattt](https://twitter.com/mattt))

Sources/DocTest/Expectation.swift

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
3+
public enum Expectation: Hashable {
4+
case value(String)
5+
case error
6+
7+
public init?(_ string: String?) {
8+
guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil }
9+
if string.starts(with: "=>"),
10+
let index = string.firstIndex(where: { $0.isWhitespace })
11+
{
12+
self = .value(string.suffix(from: index).trimmingCharacters(in: .whitespaces))
13+
} else if string.starts(with: "!!") {
14+
self = .error
15+
} else {
16+
return nil
17+
}
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import SwiftSyntax
2+
3+
extension SyntaxProtocol {
4+
var expectations: [Expectation] {
5+
let trivia: Trivia?
6+
switch self {
7+
case let node as SourceFileSyntax:
8+
trivia = node.eofToken.leadingTrivia
9+
default:
10+
trivia = leadingTrivia
11+
}
12+
13+
return trivia?.comments.compactMap { Expectation($0) } ?? []
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import SwiftSyntax
2+
3+
extension Trivia {
4+
var comments: [String] {
5+
compactMap { piece -> String? in
6+
switch piece {
7+
case let .lineComment(comment):
8+
let startIndex = comment.index(comment.startIndex, offsetBy: 2)
9+
return String(comment.suffix(from: startIndex))
10+
case let .blockComment(comment):
11+
let startIndex = comment.index(comment.startIndex, offsetBy: 2)
12+
let endIndex = comment.index(comment.endIndex, offsetBy: -2)
13+
return String(comment[startIndex ..< endIndex])
14+
default:
15+
return nil
16+
}
17+
}
18+
}
19+
20+
var expectations: [Expectation] {
21+
return comments.compactMap { Expectation($0) }
22+
}
23+
}

0 commit comments

Comments
 (0)