Skip to content

Commit 8a9582f

Browse files
committed
Initial PBT
1 parent c2aec3e commit 8a9582f

File tree

5 files changed

+203
-15
lines changed

5 files changed

+203
-15
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ bin/
4040
.vscode/
4141

4242
### Mac OS ###
43-
.DS_Store
43+
.DS_Store
44+
.jqwik-database

README.md

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
## TLA<sup>+</sup> formatter
2+
23
<p align="center"><img alt="temporary tla+ formatter logo" src="assets/tlaplus-formatter-temp-logo.jpg" width="250"></p>
34

4-
This is a formatter for the TLA<sup>+</sup> language.
5+
This is a formatter for the TLA<sup>+</sup> language.
56

6-
It uses tlaplus tools' SANY library to parse your specification, and it applies some (at the moment) predefined format to it.
7+
It uses tlaplus tools' SANY library to parse your specification, and it applies some (at the moment) predefined format
8+
to it.
79

810
This is still _ALPHA_ software, feel free to try it out and leave feedback but be aware it might break your specs.
911

1012
## Project Goals:
13+
1114
* A formatter for the TLA+ language. Pluscal is currently not a priority.
1215
* It should be configurable but also provide sane defaults.
1316
* It should never add useless chars (no extra spaces or extra newlines)
@@ -16,40 +19,71 @@ This is still _ALPHA_ software, feel free to try it out and leave feedback but b
1619
* It should be fast.
1720

1821
## Configurations:
22+
1923
If you have specific requests for configuration options you would like to have, please consider opening an issue.
2024

21-
Currently, the idea is to follow the same ideas behind rustfmt. A user level formatter config and a project level formatter config.
25+
Currently, the idea is to follow the same ideas behind rustfmt. A user level formatter config and a project level
26+
formatter config.
2227

2328
## Example
29+
2430
To see some examples of current reformatting, compare:
25-
* HourClock.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/HourClock.tla) and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/HourClock.tla)
26-
* TowerOfHanoi.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/TowerOfHanoi.tla) and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/TowerOfHanoi.tla)
27-
* Stones.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/Stones.tla) and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/Stones.tla)
2831

29-
More examples are in the test/java/resources/{inputs|outputs} folders. These sources are taken from the TLA+ Examples repo.
32+
*
33+
34+
HourClock.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/HourClock.tla)
35+
and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/HourClock.tla)
36+
37+
*
38+
39+
TowerOfHanoi.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/TowerOfHanoi.tla)
40+
and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/TowerOfHanoi.tla)
41+
42+
*
43+
44+
Stones.tla: [before](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/inputs/Stones.tla)
45+
and [after](https://github.com/FedericoPonzi/tlaplus-formatter/blob/main/src/test/resources/outputs/Stones.tla)
46+
47+
More examples are in the test/java/resources/{inputs|outputs} folders. These sources are taken from the TLA+ Examples
48+
repo.
49+
50+
## Testing with TLA+-Smith
51+
52+
This project integrates [TLA+-Smith](https://github.com/fponzi/tlaplus-smith), a random TLA+ specification generator,
53+
for comprehensive testing. TLA+-Smith helps ensure the formatter works correctly with a wide variety of TLA+ constructs
54+
and edge cases.
3055

3156
## How to run
32-
If you want to try it out, go to the build page of the latest commit, and download the "Package.zip" file from the artifact section at the bottom of the page ([Example](https://github.com/FedericoPonzi/tlaplus-formatter/actions/runs/10027954925)).
3357

34-
You will need at least java 11 (same requirement as tlatools).
58+
If you want to try it out, go to the build page of the latest commit, and download the "Package.zip" file from the
59+
artifact section at the bottom of the
60+
page ([Example](https://github.com/FedericoPonzi/tlaplus-formatter/actions/runs/10027954925)).
61+
62+
You will need at least java 11 (the same requirement as tlatools).
3563

3664
Unzip the file, and you can invoke the formatter like this:
65+
3766
```
3867
java -jar tlaplus-formatter.jar <INFILE> [OUTFILE]
3968
```
4069

41-
It will print your reformatted spec in output. If the optional "OUTFILE" parameter is specified, it will write the output to that file.
42-
You can use the input file as output file as well. Run with `-help` parameter for the help text.
70+
It will print your reformatted spec in output. If the optional "OUTFILE" parameter is specified, it will write the
71+
output to that file.
72+
You can use the input file as the output file as well. Run with `-help` parameter for the help text.
4373

4474
## VSCode integration
75+
4576
It's a work in progress, see [PR-327](https://github.com/tlaplus/vscode-tlaplus/pull/327/files) on vscode-tlaplus repo.
4677

4778
## Limitations
48-
Because it uses SANY underneath (TLC's parser), your spec needs to first succeed SANY's
49-
parsing process, otherwise the formatter won't be able to reformat your file.
79+
80+
Because it uses SANY underneath (TLC's parser), your spec needs to first succeed SANY's
81+
parsing process; otherwise the formatter won't be able to reformat your file.
5082

5183
---
5284

5385
## Development
54-
Some of the `constants` used in the code are coming from SANY's codebase, specifically from this file: `src/tla2sany/st/SyntaxTreeConstants.java` (check [tlaplus repo](https://github.com/tlaplus/tlaplus/)).
86+
87+
Some of the `constants` used in the code are coming from SANY's codebase, specifically from this file:
88+
`src/tla2sany/st/SyntaxTreeConstants.java` (check [tlaplus repo](https://github.com/tlaplus/tlaplus/)).
5589

TESTING.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Testing Guide
2+
3+
This document describes the testing approach for the TLA+ formatter.
4+
5+
## Test Types
6+
7+
### Unit Tests
8+
Individual component tests located in `src/test/java/me/fponzi/tlaplusformatter/` that test specific lexicon elements and formatting constructs.
9+
10+
### Integration Tests
11+
End-to-end tests that format complete TLA+ specifications and compare against expected outputs in `src/test/resources/`.
12+
13+
### Property-Based Tests
14+
Automated tests using [jqwik](https://jqwik.net/) that generate hundreds of random TLA+ specifications to verify formatter correctness.
15+
16+
## Property-Based Testing
17+
18+
The `PropertyBasedFormatterTest` class uses property-based testing to automatically verify formatter behavior across a wide range of inputs. Property-based testing generates random test data and verifies that certain properties always hold, making it excellent for finding edge cases that manual testing might miss.
19+
20+
The implementation generates syntactically valid TLA+ specifications with:
21+
- Random module names
22+
- 0-3 constant declarations
23+
- Simple operator definitions using the declared constants
24+
25+
Two critical properties are tested:
26+
27+
**Semantic Preservation**: The formatter must preserve the meaning of the code. This is verified by parsing both the original and formatted specifications into Abstract Syntax Trees (ASTs) and ensuring they are structurally identical.
28+
29+
**Idempotence**: Running the formatter multiple times should produce the same result. This ensures the formatter reaches a stable state and doesn't continuously modify the code.
30+
31+
### Running Property-Based Tests
32+
33+
```bash
34+
# Run all property-based tests (1000 examples each by default)
35+
./gradlew test --tests PropertyBasedFormatterTest
36+
37+
# Run with specific seed for reproducible results
38+
./gradlew test --tests PropertyBasedFormatterTest -Djqwik.seeds=123456789
39+
40+
# Run all tests
41+
./gradlew test
42+
```
43+
44+
The property-based tests automatically run 1000 random examples by default and include edge case testing, providing comprehensive coverage with minimal test code.
45+
46+
## Running Tests
47+
48+
```bash
49+
# Run all tests
50+
./gradlew test
51+
52+
# Run specific test class
53+
./gradlew test --tests FormatterE2ETest
54+
55+
# Build and test
56+
./gradlew build
57+
```

build.gradle.kts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,35 @@ plugins {
66
group = "me.fponzi"
77
version = "1.0-SNAPSHOT"
88

9+
java {
10+
sourceCompatibility = JavaVersion.VERSION_11
11+
targetCompatibility = JavaVersion.VERSION_11
12+
}
13+
914
repositories {
15+
mavenLocal()
1016
mavenCentral()
1117
// Add the repository for the snapshot dependency
1218
maven {
1319
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
1420
}
21+
maven {
22+
url = uri("https://jitpack.io")
23+
}
1524
}
1625

1726
dependencies {
1827
implementation("org.lamport:tla2tools:1.8.0-SNAPSHOT")
1928
implementation("commons-io:commons-io:2.16.1")
29+
testImplementation("com.github.FedericoPonzi:tlaplus-smith:main-SNAPSHOT") {
30+
isChanging = true
31+
}
32+
2033
testImplementation(platform("org.junit:junit-bom:5.10.0"))
2134
testImplementation("org.junit.jupiter:junit-jupiter")
2235
testImplementation("org.mockito:mockito-core:5.7.0")
2336
testImplementation("org.mockito:mockito-junit-jupiter:5.7.0")
37+
testImplementation("net.jqwik:jqwik:1.8.0")
2438
implementation("commons-cli:commons-cli:1.8.0")
2539

2640
// Logging
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package me.fponzi.tlaplusformatter;
2+
3+
import me.fponzi.tlaplusformatter.exceptions.SanyFrontendException;
4+
import me.fponzi.tlasmith.TLASmith;
5+
import net.jqwik.api.*;
6+
import org.junit.jupiter.api.Disabled;
7+
import tla2sany.st.TreeNode;
8+
9+
import java.io.IOException;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
class PropertyBasedFormatterTest extends LexiconTest {
15+
// this is just for debugging purposes
16+
// it will print out some of the generated specs and their formatted versions
17+
@Property(tries = 3)
18+
@Disabled
19+
void showExampleSpecs(@ForAll("validTlaSpecs")
20+
String spec)
21+
throws IOException,
22+
SanyFrontendException {
23+
System.err.println("=== Generated TLA+ Spec ===");
24+
System.err.println(spec);
25+
System.err.println("=== After Formatting ===");
26+
try {
27+
System.err.println(format(spec));
28+
} catch (Exception e) {
29+
System.err.println("FORMATTING ERROR: " + e.getMessage());
30+
System.err.println("Failed spec: " + spec);
31+
}
32+
System.err.println("*******************");
33+
}
34+
35+
@Property
36+
void formatterPreservesSemantics(@ForAll("validTlaSpecs") String spec)
37+
throws IOException, SanyFrontendException {
38+
try {
39+
String formatted = format(spec);
40+
TreeNode originalAst = parse(spec);
41+
TreeNode formattedAst = parse(formatted);
42+
43+
assertTrue(compareAst(originalAst, formattedAst),
44+
"AST should be preserved after formatting");
45+
} catch (Exception e) {
46+
System.err.println("Failed spec: " + spec);
47+
throw e;
48+
}
49+
}
50+
51+
@Property
52+
void formatterIsIdempotent(@ForAll("validTlaSpecs") String spec)
53+
throws IOException, SanyFrontendException {
54+
String formatted = format(spec);
55+
String doubleFormatted = format(formatted);
56+
57+
assertEquals(formatted, doubleFormatted,
58+
"Formatter should be idempotent");
59+
}
60+
61+
private String format(String spec) throws IOException, SanyFrontendException {
62+
return new TLAPlusFormatter(spec).getOutput();
63+
}
64+
65+
private TreeNode parse(String spec) throws IOException, SanyFrontendException {
66+
return new TLAPlusFormatter(spec).root;
67+
}
68+
69+
@Provide
70+
Arbitrary<String> validTlaSpecs() {
71+
return Arbitraries.integers().between(1, 10000).map(seed -> {
72+
try {
73+
return TLASmith.toTLAString(TLASmith.generateSpec("PropTest", seed));
74+
} catch (Exception e) {
75+
System.err.println("Failed to generate spec with seed: " + seed);
76+
throw e;
77+
}
78+
});
79+
}
80+
81+
82+
}

0 commit comments

Comments
 (0)