Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import javax.swing.BorderFactory;
import javax.swing.Box;
Expand Down Expand Up @@ -371,6 +372,7 @@ private void runValidation() {
private ValidationRunnerConfig buildConfig() throws URISyntaxException {
ValidationRunnerConfig.Builder config = ValidationRunnerConfig.builder();
config.setPrettyJson(true);
config.setStdoutOutput(false);

String gtfsInput = gtfsInputField.getText();
if (gtfsInput.isBlank()) {
Expand All @@ -386,7 +388,7 @@ private ValidationRunnerConfig buildConfig() throws URISyntaxException {
if (outputDirectory.isBlank()) {
throw new IllegalStateException("outputDirectoryField is blank");
}
config.setOutputDirectory(Path.of(outputDirectory));
config.setOutputDirectory(Optional.of(Path.of(outputDirectory)));

Object numThreads = numThreadsSpinner.getValue();
if (numThreads instanceof Integer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ void handleResult(ValidationRunnerConfig config, ValidationRunner.Status status)
handleError();
}

Path reportPath = config.htmlReportPath();
Path reportPath = config.htmlReportPath().orElse(null);
if (status == ValidationRunner.Status.SYSTEM_ERRORS) {
reportPath = config.systemErrorsReportPath();
reportPath = config.systemErrorsReportPath().orElse(null);
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void testValidationConfig() throws URISyntaxException {

ValidationRunnerConfig config = configCaptor.getValue();
assertThat(config.gtfsSource()).isEqualTo(new URI("http://transit/gtfs.zip"));
assertThat(config.outputDirectory()).isEqualTo(Path.of("/path/to/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/path/to/output"));
assertThat(config.numThreads()).isEqualTo(1);
assertThat(config.countryCode().isUnknown()).isTrue();
}
Expand All @@ -84,7 +84,7 @@ public void testValidationConfigWithAdvancedOptions() throws URISyntaxException

ValidationRunnerConfig config = configCaptor.getValue();
assertThat(config.gtfsSource()).isEqualTo(new URI("http://transit/gtfs.zip"));
assertThat(config.outputDirectory()).isEqualTo(Path.of("/path/to/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/path/to/output"));
assertThat(config.numThreads()).isEqualTo(5);
assertThat(config.countryCode().getCountryCode()).isEqualTo("US");
}
Expand Down
4 changes: 4 additions & 0 deletions cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ shadowJar {
// from minimization.
exclude(project(':main'))
exclude(dependency(libs.httpclient5.get().toString()))
// Keep SLF4J implementation to avoid warnings
// Note that SLF4J comes from some transitive dependencies
exclude(dependency('org.slf4j:slf4j-jdk14'))
}
// Change the JAR name from 'main' to 'gtfs-validator'
archiveBaseName = rootProject.name
Expand All @@ -55,6 +58,7 @@ dependencies {
implementation libs.flogger
implementation libs.flogger.system.backend
implementation libs.guava
implementation libs.slf4j.jul

testImplementation libs.junit
testImplementation libs.truth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import org.mobilitydata.gtfsvalidator.input.CountryCode;
import org.mobilitydata.gtfsvalidator.runner.ValidationRunnerConfig;

Expand All @@ -38,8 +39,7 @@ public class Arguments {

@Parameter(
names = {"-o", "--output_base"},
description = "Base directory to store the outputs",
required = true)
description = "Base directory to store the outputs (required if not using --stdout)")
private String outputBase;

@Parameter(
Expand Down Expand Up @@ -110,6 +110,11 @@ public class Arguments {
description = "Skips check for new validator version")
private boolean skipValidatorUpdate = false;

@Parameter(
names = {"--stdout"},
description = "Output JSON report to stdout instead of writing to files (conflicts with -o)")
private boolean stdoutOutput = false;

ValidationRunnerConfig toConfig() throws URISyntaxException {
ValidationRunnerConfig.Builder builder = ValidationRunnerConfig.builder();
if (input != null) {
Expand All @@ -121,7 +126,10 @@ ValidationRunnerConfig toConfig() throws URISyntaxException {
}
}
if (outputBase != null) {
builder.setOutputDirectory(Path.of(outputBase));
builder.setOutputDirectory(Optional.of(Path.of(outputBase)));
} else if (stdoutOutput) {
// When using stdout, no output directory is needed
builder.setOutputDirectory(Optional.empty());
}
if (countryCode != null) {
builder.setCountryCode(CountryCode.forStringOrUnknown(countryCode));
Expand All @@ -141,6 +149,7 @@ ValidationRunnerConfig toConfig() throws URISyntaxException {
builder.setNumThreads(numThreads);
builder.setPrettyJson(pretty);
builder.setSkipValidatorUpdate(skipValidatorUpdate);
builder.setStdoutOutput(stdoutOutput);
return builder.build();
}

Expand All @@ -160,6 +169,10 @@ public boolean getExportNoticeSchema() {
return exportNoticeSchema;
}

public boolean getStdoutOutput() {
return stdoutOutput;
}

/**
* @return true if CLI parameter combination is legal, otherwise return false
*/
Expand All @@ -185,6 +198,16 @@ public boolean validate() {
return false;
}

if (stdoutOutput && outputBase != null) {
logger.atSevere().log("Cannot use --stdout with --output_base. Use one or the other.");
return false;
}

if (outputBase == null && !stdoutOutput) {
logger.atSevere().log("Must provide either --output_base or --stdout");
return false;
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
Expand All @@ -45,7 +45,7 @@ public void shortNameShouldInitializeArguments() throws URISyntaxException {
new JCommander(underTest).parse(commandLineArgumentAsStringArray);
ValidationRunnerConfig config = underTest.toConfig();
assertThat(config.gtfsSource()).isEqualTo(toFileUri("/tmp/gtfs.zip"));
assertThat((Object) config.outputDirectory()).isEqualTo(Path.of("/tmp/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/tmp/output"));
assertThat(config.countryCode()).isEqualTo(CountryCode.forStringOrUnknown("au"));
assertThat(config.numThreads()).isEqualTo(4);
assertThat(config.validationReportFileName()).matches("report.json");
Expand All @@ -72,7 +72,7 @@ public void shortNameShouldInitializeArguments_url() throws URISyntaxException {
new JCommander(underTest).parse(commandLineArgumentAsStringArray);
ValidationRunnerConfig config = underTest.toConfig();
assertThat(config.gtfsSource()).isEqualTo(new URI("http://host/gtfs.zip"));
assertThat((Object) config.outputDirectory()).isEqualTo(Path.of("/tmp/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/tmp/output"));
assertThat(config.countryCode()).isEqualTo(CountryCode.forStringOrUnknown("au"));
assertThat(config.numThreads()).isEqualTo(4);
assertThat(config.storageDirectory()).hasValue(Path.of("/tmp/storage"));
Expand All @@ -95,7 +95,7 @@ public void longNameShouldInitializeArguments() throws URISyntaxException {
new JCommander(underTest).parse(commandLineArgumentAsStringArray);
ValidationRunnerConfig config = underTest.toConfig();
assertThat(config.gtfsSource()).isEqualTo(toFileUri("/tmp/gtfs.zip"));
assertThat((Object) config.outputDirectory()).isEqualTo(Path.of("/tmp/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/tmp/output"));
assertThat(config.countryCode()).isEqualTo(CountryCode.forStringOrUnknown("ca"));
assertThat(config.numThreads()).isEqualTo(4);
assertThat(config.validationReportFileName()).matches("report.json");
Expand Down Expand Up @@ -131,7 +131,7 @@ public void longNameShouldInitializeArguments_url() throws URISyntaxException {
new JCommander(underTest).parse(commandLineArgumentAsStringArray);
ValidationRunnerConfig config = underTest.toConfig();
assertThat(config.gtfsSource()).isEqualTo(new URI("http://host/gtfs.zip"));
assertThat((Object) config.outputDirectory()).isEqualTo(Path.of("/tmp/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/tmp/output"));
assertThat(config.countryCode()).isEqualTo(CountryCode.forStringOrUnknown("ca"));
assertThat(config.numThreads()).isEqualTo(4);
assertThat(config.storageDirectory()).hasValue(Path.of("/tmp/storage"));
Expand All @@ -152,7 +152,7 @@ public void numThreadsShouldHaveDefaultValueIfNotProvided() throws URISyntaxExce
new JCommander(underTest).parse(commandLineArgumentAsStringArray);
ValidationRunnerConfig config = underTest.toConfig();
assertThat(config.gtfsSource()).isEqualTo(toFileUri("/tmp/gtfs.zip"));
assertThat((Object) config.outputDirectory()).isEqualTo(Path.of("/tmp/output"));
assertThat(config.outputDirectory()).hasValue(Path.of("/tmp/output"));
assertThat(config.countryCode()).isEqualTo(CountryCode.forStringOrUnknown("ca"));
assertThat(config.numThreads()).isEqualTo(1);
}
Expand Down Expand Up @@ -182,7 +182,7 @@ public void noUrlNoInput_long_isNotValid() {

@Test
public void noArguments_isNotValid() {
assertThrows(ParameterException.class, () -> validateArguments(new String[] {}));
assertThat(validateArguments(new String[] {})).isFalse();
}

@Test
Expand Down Expand Up @@ -259,4 +259,39 @@ public void exportNoticesSchema_schemaAndValidation() {
assertThat(args.getExportNoticeSchema()).isTrue();
assertThat(args.abortAfterNoticeSchemaExport()).isFalse();
}

@Test
public void testStdoutOutput() {
String[] commandLineArgumentAsStringArray = {"--input", "test.zip", "--stdout"};

Arguments args = new Arguments();
new JCommander(args).parse(commandLineArgumentAsStringArray);

assertTrue(args.validate());
assertTrue(args.getStdoutOutput());
}

@Test
public void testStdoutOutputWithOutputBaseConflict() {
String[] commandLineArgumentAsStringArray = {
"--input", "test.zip",
"--output_base", "/tmp/output",
"--stdout"
};

Arguments args = new Arguments();
new JCommander(args).parse(commandLineArgumentAsStringArray);

assertFalse(args.validate());
}

@Test
public void testStdoutOutputWithoutInput() {
String[] commandLineArgumentAsStringArray = {"--stdout"};

Arguments args = new Arguments();
new JCommander(args).parse(commandLineArgumentAsStringArray);

assertFalse(args.validate());
}
}
29 changes: 29 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
| `-e` | `--system_errors_report_name` | Optional | Name of the system errors report (including `.json` extension). |
| `-n` | `--export_notices_schema` | Optional | Export notice schema as a json file. |
| `-p` | `--pretty` | Optional | Pretty JSON validation report. If specified, the JSON validation report will be printed using JSON Pretty print. This does not impact data parsing. |
| `--stdout` | `--stdout` | Optional | Output JSON report to stdout instead of writing to files. Use with `-i` or `-u` but not with `-o`. Enables piping to tools like `jq`. |
| `-d` | `--date` | Optional | The date used to validate the feed for time-based rules, e.g feed_expiration_30_days, in ISO_LOCAL_DATE format like '2001-01-30'. By default, the current date is used. |
| `-svu` | `--skip_validator_update` | Optional | Skip GTFS version validation update check. If specified, the GTFS version validation will be skipped. By default, the GTFS version validation will be performed. |

Expand Down Expand Up @@ -61,6 +62,34 @@ java -jar gtfs-validator-v2.0.jar -u https://url/to/dataset.zip -o relative/outp
1. Validate the GTFS data and output the results to the directory located at `relative/output/path`. Validation results are exported to JSON by default.
Please note that since downloading will take time, we recommend validating repeatedly on a local file.

## via stdout output (for scripting and piping)

The `--stdout` option outputs JSON directly to stdout instead of writing files, making it ideal for scripting and piping to other tools.

### Basic stdout usage
```
java -jar gtfs-validator-v2.0.jar -i relative/path/to/dataset.zip --stdout
```

### Pipe to jq for processing
```
java -jar gtfs-validator-v2.0.jar -i relative/path/to/dataset.zip --stdout | jq '.summary.validationTimeSeconds'
```

### Pretty JSON output to stdout
```
java -jar gtfs-validator-v2.0.jar -i relative/path/to/dataset.zip --stdout --pretty
```

### URL-based input with stdout
```
java -jar gtfs-validator-v2.0.jar -u https://url/to/dataset.zip --stdout
```

⚠️ Note that `--stdout` cannot be used with `-o` or `--output_base`. Use one or the other.

⚠️ When using `--stdout`, all system errors and logging output are suppressed to ensure clean JSON output. Only severe-level log messages (hard crashes) will appear on stderr.

## via GitHub Actions - Run the validator on any gtfs archive available on a public url

1. [Fork this repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo)
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j
aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "aspectjrt" }
aspectjrt-weaver = { module = "org.aspectj:aspectjweaver", version.ref = "aspectjrtweaver" }
findbugs = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jacksonDatabind" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jacksonDatabind" }
slf4j-jul = { module = "org.slf4j:slf4j-jdk14", version = "1.7.25" }
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public JsonReportSummaryGenerator(
date,
config != null ? config.gtfsSource().toString() : null,
config != null ? config.numThreads() : 0,
config != null ? config.outputDirectory().toString() : null,
config != null && config.outputDirectory().isPresent()
? config.outputDirectory().get().toString()
: null,
config != null ? config.systemErrorsReportFileName() : null,
config != null ? config.validationReportFileName() : null,
config != null ? config.htmlReportFileName() : null,
Comment on lines 37 to 39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep consistency, we can set the exported filenames as they will not be created with the stdout parameter. Please take the suggested code snippet as a guide, I'm not sure if it has the right alignment or any other issue, GitHub editor has its limitations ;-)

Suggested change
config != null ? config.systemErrorsReportFileName() : null,
config != null ? config.validationReportFileName() : null,
config != null ? config.htmlReportFileName() : null,
config != null && !config.stdoutOutput() ? config.systemErrorsReportFileName() : null,
config != null && !config.stdoutOutput() ? config.validationReportFileName() : null,
config != null && !config.stdoutOutput() ? config.htmlReportFileName() : null,

Expand Down
Loading