diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoader.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoader.java index 91a16f01e8..f8059d9683 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoader.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoader.java @@ -20,6 +20,7 @@ import com.google.common.flogger.FluentLogger; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,6 +48,12 @@ public class GtfsFeedLoader { private final HashMap> tableDescriptors = new HashMap<>(); private int numThreads = 1; + /** + * The set of validators that were skipped during validation because their file dependencies had + * parse errors. + */ + private final List> skippedValidators = new ArrayList<>(); + public GtfsFeedLoader( ImmutableList>> tableDescriptorClasses) { for (Class> clazz : tableDescriptorClasses) { @@ -72,6 +79,10 @@ public void setNumThreads(int numThreads) { this.numThreads = numThreads; } + public List> getSkippedValidators() { + return Collections.unmodifiableList(skippedValidators); + } + @SuppressWarnings("unchecked") public GtfsFeedContainer loadAndValidate( GtfsInput gtfsInput, ValidatorProvider validatorProvider, NoticeContainer noticeContainer) @@ -132,17 +143,11 @@ public GtfsFeedContainer loadAndValidate( } } GtfsFeedContainer feed = new GtfsFeedContainer(tableContainers); - if (!feed.isParsedSuccessfully()) { - // No need to call file validators if any file failed to parse. File validations in that - // case may lead to confusing error messages. - // - // Consider we failed to parse a row trip.txt but there is another row in stop_times.txt - // that references a trip. Then foreign key validator may notify about a missing trip_id - // which would be wrong. - return feed; - } List> validatorCallables = new ArrayList<>(); - for (FileValidator validator : validatorProvider.createMultiFileValidators(feed)) { + // Validators with parser-error dependencies will not be returned here, but instead added to + // the skippedValidators list. + for (FileValidator validator : + validatorProvider.createMultiFileValidators(feed, skippedValidators::add)) { validatorCallables.add( () -> { NoticeContainer validatorNotices = new NoticeContainer(); diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/testing/MockGtfs.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/testing/MockGtfs.java new file mode 100644 index 0000000000..ab4b4ffdad --- /dev/null +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/testing/MockGtfs.java @@ -0,0 +1,64 @@ +package org.mobilitydata.gtfsvalidator.testing; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class MockGtfs { + private final File file; + + private final Map _fileContentsByName = new HashMap<>(); + + private MockGtfs(File file) { + this.file = file; + } + + public static MockGtfs create() throws IOException { + File file = File.createTempFile("MockGtfs-", ".zip"); + file.deleteOnExit(); + return new MockGtfs(file); + } + + public File getFile() { + return file; + } + + public Path getPath() { + return file.toPath(); + } + + public void putFileFromString(String fileName, String content) { + _fileContentsByName.put(fileName, content.getBytes(StandardCharsets.UTF_8)); + updateZipContents(); + } + + public void putFileFromLines(String fileName, String... lines) { + putFileFromString(fileName, String.join("\n", lines)); + } + + private void updateZipContents() { + try { + if (file.exists()) { + file.delete(); + } + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file)); + for (Map.Entry entry : _fileContentsByName.entrySet()) { + String fileName = entry.getKey(); + byte[] content = entry.getValue(); + ZipEntry zipEntry = new ZipEntry(fileName); + out.putNextEntry(zipEntry); + out.write(content); + out.closeEntry(); + } + out.close(); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } +} diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProvider.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProvider.java index 130fc0aa1a..e694fc9c02 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProvider.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProvider.java @@ -20,9 +20,11 @@ import com.google.common.flogger.FluentLogger; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.mobilitydata.gtfsvalidator.table.GtfsEntity; import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer; import org.mobilitydata.gtfsvalidator.table.GtfsTableContainer; +import org.mobilitydata.gtfsvalidator.validator.ValidatorLoader.ValidatorWithDependencyStatus; /** Default implementation of {@link ValidatorProvider}. */ public class DefaultValidatorProvider implements ValidatorProvider { @@ -109,13 +111,19 @@ public List createSingleFileValidators( } @Override - public List createMultiFileValidators(GtfsFeedContainer feed) { + public List createMultiFileValidators( + GtfsFeedContainer feed, Consumer> skippedValidators) { ArrayList validators = new ArrayList<>(); validators.ensureCapacity(multiFileValidators.size()); for (Class validatorClass : multiFileValidators) { try { - validators.add( - ValidatorLoader.createMultiFileValidator(validatorClass, feed, validationContext)); + ValidatorWithDependencyStatus validatorWithStatus = + ValidatorLoader.createMultiFileValidator(validatorClass, feed, validationContext); + if (validatorWithStatus.dependenciesHaveErrors()) { + skippedValidators.accept(validatorClass); + } else { + validators.add(validatorWithStatus.validator()); + } } catch (ReflectiveOperationException e) { logger.atSevere().withCause(e).log( "Cannot instantiate validator %s", validatorClass.getCanonicalName()); diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoader.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoader.java index 5ce79a045b..c80968a304 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoader.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoader.java @@ -26,7 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.function.Function; +import javax.annotation.Nullable; import javax.inject.Inject; import org.mobilitydata.gtfsvalidator.input.CountryCode; import org.mobilitydata.gtfsvalidator.input.CurrentDateTime; @@ -169,16 +169,9 @@ private static Constructor chooseConstructor(Class validatorClass) validatorClass.getCanonicalName())); } - /** - * Instantiates a validator of given class and injects its dependencies. - * - * @param clazz validator class to instantiate - * @param provider dependency provider - * @param type of the validator to instantiate - * @return a new validator - */ - private static T createValidator(Class clazz, Function, Object> provider) - throws ReflectiveOperationException { + /** Instantiates a validator of given class and injects its dependencies. */ + private static ValidatorWithDependencyStatus createValidator( + Class clazz, DependencyResolver dependencyResolver) throws ReflectiveOperationException { Constructor chosenConstructor; try { chosenConstructor = chooseConstructor(clazz); @@ -189,10 +182,13 @@ private static T createValidator(Class clazz, Function, Object> // Inject constructor parameters. Object[] parameters = new Object[chosenConstructor.getParameterCount()]; for (int i = 0; i < parameters.length; ++i) { - parameters[i] = provider.apply(chosenConstructor.getParameters()[i].getType()); + parameters[i] = + dependencyResolver.resolveDependency(chosenConstructor.getParameters()[i].getType()); } chosenConstructor.setAccessible(true); - return chosenConstructor.newInstance(parameters); + T validator = chosenConstructor.newInstance(parameters); + return new ValidatorWithDependencyStatus( + validator, dependencyResolver.dependenciesHaveErrors); } /** @@ -205,7 +201,8 @@ private static T createValidator(Class clazz, Function, Object> */ public static T createValidatorWithContext( Class clazz, ValidationContext validationContext) throws ReflectiveOperationException { - return createValidator(clazz, validationContext::get); + return createValidator(clazz, new DependencyResolver(validationContext, null, null)) + .validator(); } /** Instantiates a {@code FileValidator} for a single table. */ @@ -214,29 +211,81 @@ public static FileValidator createSingleFileValidator( GtfsTableContainer table, ValidationContext validationContext) throws ReflectiveOperationException { - return createValidator( - clazz, - parameterClass -> - parameterClass.isAssignableFrom(table.getClass()) - ? table - : validationContext.get(parameterClass)); + return createValidator(clazz, new DependencyResolver(validationContext, table, null)) + .validator(); } /** Instantiates a {@code FileValidator} for multiple tables in a given feed. */ @SuppressWarnings("unchecked") - public static FileValidator createMultiFileValidator( - Class clazz, - GtfsFeedContainer feed, - ValidationContext validationContext) + public static ValidatorWithDependencyStatus createMultiFileValidator( + Class clazz, GtfsFeedContainer feed, ValidationContext validationContext) throws ReflectiveOperationException { - return createValidator( - clazz, - parameterClass -> - GtfsFeedContainer.class.isAssignableFrom(parameterClass) - ? feed - : GtfsTableContainer.class.isAssignableFrom(parameterClass) - ? feed.getTable((Class>) parameterClass) - : validationContext.get(parameterClass)); + return createValidator(clazz, new DependencyResolver(validationContext, null, feed)); + } + + /** + * Helper class for resolving injected dependencies of validators, while also tracking if those + * dependencies have blocking errors. + */ + private static class DependencyResolver { + private final ValidationContext context; + @Nullable private final GtfsTableContainer tableContainer; + @Nullable private final GtfsFeedContainer feedContainer; + + /** This will be set to true if a resolved dependency was not parsed successfully. */ + private boolean dependenciesHaveErrors = false; + + public DependencyResolver( + ValidationContext context, + @Nullable GtfsTableContainer tableContainer, + @Nullable GtfsFeedContainer feedContainer) { + this.context = context; + this.tableContainer = tableContainer; + this.feedContainer = feedContainer; + } + + public Object resolveDependency(Class parameterClass) { + if (feedContainer != null && parameterClass.isAssignableFrom(GtfsFeedContainer.class)) { + if (!feedContainer.isParsedSuccessfully()) { + dependenciesHaveErrors = true; + } + return feedContainer; + } + if (tableContainer != null && parameterClass.isAssignableFrom(tableContainer.getClass())) { + if (!tableContainer.isParsedSuccessfully()) { + dependenciesHaveErrors = true; + } + return tableContainer; + } + if (feedContainer != null && GtfsTableContainer.class.isAssignableFrom(parameterClass)) { + GtfsTableContainer container = + feedContainer.getTable((Class>) parameterClass); + if (container != null && !container.isParsedSuccessfully()) { + dependenciesHaveErrors = true; + } + return container; + } + return context.get(parameterClass); + } + } + + public static final class ValidatorWithDependencyStatus { + private final T validator; + // Will be true if one of the injected dependencies of the validator has parse errors. + private final boolean dependenciesHaveErrors; + + public ValidatorWithDependencyStatus(T validator, boolean dependenciesHaveErrors) { + this.validator = validator; + this.dependenciesHaveErrors = dependenciesHaveErrors; + } + + public T validator() { + return validator; + } + + public boolean dependenciesHaveErrors() { + return dependenciesHaveErrors; + } } /** Describes all loaded validators. */ diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorProvider.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorProvider.java index 0c4e377e21..c928f67e26 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorProvider.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorProvider.java @@ -17,6 +17,7 @@ package org.mobilitydata.gtfsvalidator.validator; import java.util.List; +import java.util.function.Consumer; import org.mobilitydata.gtfsvalidator.table.GtfsEntity; import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer; import org.mobilitydata.gtfsvalidator.table.GtfsTableContainer; @@ -59,11 +60,15 @@ List createSingleFileValidators( GtfsTableContainer table); /** - * Creates a list of cross-table validators. + * Creates a list of cross-table validators. Any validator that has a dependency with parse errors + * will not be returned by this method, but instead noted with a call to the `skippedValidators` + * callback. * *

Use {@link ValidatorUtil#safeValidate} to invoke each validator. * * @param feed GTFS feed to validate + * @param skippedValidators */ - List createMultiFileValidators(GtfsFeedContainer feed); + List createMultiFileValidators( + GtfsFeedContainer feed, Consumer> skippedValidators); } diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/TestUtils.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/TestUtils.java index af024539fa..8bb3e7b16c 100644 --- a/core/src/test/java/org/mobilitydata/gtfsvalidator/TestUtils.java +++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/TestUtils.java @@ -5,9 +5,13 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; import java.util.List; +import org.mobilitydata.gtfsvalidator.input.CountryCode; +import org.mobilitydata.gtfsvalidator.input.CurrentDateTime; import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.validator.ValidationContext; public class TestUtils { @@ -24,4 +28,11 @@ public static List> validationNoticeTypes( public static InputStream toInputStream(String s) { return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); } + + public static ValidationContext contextForTest() { + return ValidationContext.builder() + .setCountryCode(CountryCode.forStringOrUnknown("ca")) + .setCurrentDateTime(new CurrentDateTime(ZonedDateTime.now())) + .build(); + } } diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoaderTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoaderTest.java new file mode 100644 index 0000000000..339d07a960 --- /dev/null +++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/table/GtfsFeedLoaderTest.java @@ -0,0 +1,78 @@ +package org.mobilitydata.gtfsvalidator.table; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mobilitydata.gtfsvalidator.TestUtils; +import org.mobilitydata.gtfsvalidator.input.GtfsInput; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestEntity; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestEntityValidator; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestFileValidator; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestTableContainer; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestTableDescriptor; +import org.mobilitydata.gtfsvalidator.testgtfs.WholeFeedValidator; +import org.mobilitydata.gtfsvalidator.testing.MockGtfs; +import org.mobilitydata.gtfsvalidator.validator.DefaultValidatorProvider; +import org.mobilitydata.gtfsvalidator.validator.ValidatorLoader; +import org.mobilitydata.gtfsvalidator.validator.ValidatorProvider; + +@RunWith(JUnit4.class) +public class GtfsFeedLoaderTest { + private static final ImmutableList> VALIDATOR_CLASSES = + ImmutableList.of( + GtfsTestEntityValidator.class, GtfsTestFileValidator.class, WholeFeedValidator.class); + + private MockGtfs mockGtfs; + + @Before + public void before() throws IOException { + mockGtfs = MockGtfs.create(); + } + + @Test + public void testEndToEnd() throws Exception { + mockGtfs.putFileFromLines(GtfsTestEntity.FILENAME, "id,code", "1,alpha"); + GtfsInput input = GtfsInput.createFromPath(mockGtfs.getPath()); + + ValidatorProvider provider = + new DefaultValidatorProvider( + TestUtils.contextForTest(), ValidatorLoader.createForClasses(VALIDATOR_CLASSES)); + NoticeContainer notices = new NoticeContainer(); + + GtfsFeedLoader loader = new GtfsFeedLoader(ImmutableList.of(GtfsTestTableDescriptor.class)); + GtfsFeedContainer feedContainer = loader.loadAndValidate(input, provider, notices); + + GtfsTestTableContainer container = feedContainer.getTable(GtfsTestTableContainer.class); + assertThat(container.getEntities()).hasSize(1); + + GtfsTestEntity entity = container.getEntities().get(0); + assertThat(entity.id()).isEqualTo("1"); + assertThat(entity.code()).isEqualTo("alpha"); + } + + @Test + public void testInvalidDataInTable() throws Exception { + // Missing `id` value in table, which is required. + mockGtfs.putFileFromLines(GtfsTestEntity.FILENAME, "id,code", ",alpha"); + GtfsInput input = GtfsInput.createFromPath(mockGtfs.getPath()); + + ValidatorProvider provider = + new DefaultValidatorProvider( + TestUtils.contextForTest(), ValidatorLoader.createForClasses(VALIDATOR_CLASSES)); + NoticeContainer notices = new NoticeContainer(); + + GtfsFeedLoader loader = new GtfsFeedLoader(ImmutableList.of(GtfsTestTableDescriptor.class)); + GtfsFeedContainer feedContainer = loader.loadAndValidate(input, provider, notices); + + GtfsTestTableContainer container = feedContainer.getTable(GtfsTestTableContainer.class); + assertThat(container.getEntities()).isEmpty(); + + assertThat(loader.getSkippedValidators()).containsExactly(WholeFeedValidator.class); + } +} diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestEntity.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestEntity.java index 9fde5a4607..2e0fab653e 100644 --- a/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestEntity.java +++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/testgtfs/GtfsTestEntity.java @@ -57,6 +57,10 @@ public boolean hasId() { return (bitField0_ & 0x1) != 0; } + public String code() { + return code; + } + public boolean hasCode() { return (bitField0_ & 0x2) != 0; } diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProviderTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProviderTest.java new file mode 100644 index 0000000000..2c462a32b1 --- /dev/null +++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/DefaultValidatorProviderTest.java @@ -0,0 +1,74 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mobilitydata.gtfsvalidator.TestUtils; +import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsTableContainer.TableStatus; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestEntity; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestEntityValidator; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestFileValidator; +import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestTableContainer; +import org.mobilitydata.gtfsvalidator.testgtfs.WholeFeedValidator; + +@RunWith(JUnit4.class) +public class DefaultValidatorProviderTest { + + @Test + public void testCreateValidators() throws ValidatorLoaderException { + DefaultValidatorProvider provider = + new DefaultValidatorProvider( + TestUtils.contextForTest(), + ValidatorLoader.createForClasses( + ImmutableList.of( + GtfsTestEntityValidator.class, + GtfsTestFileValidator.class, + WholeFeedValidator.class))); + + GtfsTestTableContainer tableContainer = + new GtfsTestTableContainer(TableStatus.PARSABLE_HEADERS_AND_ROWS); + GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(tableContainer)); + + assertThat( + provider.createSingleEntityValidators(GtfsTestEntity.class).stream() + .map(Object::getClass)) + .containsExactly(GtfsTestEntityValidator.class); + + assertThat(provider.createSingleFileValidators(tableContainer).stream().map(Object::getClass)) + .containsExactly(GtfsTestFileValidator.class); + + List> skippedValidators = new ArrayList<>(); + assertThat( + provider.createMultiFileValidators(feedContainer, skippedValidators::add).stream() + .map(Object::getClass)) + .containsExactly(WholeFeedValidator.class); + assertThat(skippedValidators).isEmpty(); + } + + @Test + public void testCreateValidators_skippedValidators() throws ValidatorLoaderException { + DefaultValidatorProvider provider = + new DefaultValidatorProvider( + TestUtils.contextForTest(), + ValidatorLoader.createForClasses( + ImmutableList.of( + GtfsTestEntityValidator.class, + GtfsTestFileValidator.class, + WholeFeedValidator.class))); + + // Our table had invalid data! + GtfsTestTableContainer tableContainer = new GtfsTestTableContainer(TableStatus.UNPARSABLE_ROWS); + GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(tableContainer)); + + List> skippedValidators = new ArrayList<>(); + assertThat(provider.createMultiFileValidators(feedContainer, skippedValidators::add)).isEmpty(); + assertThat(skippedValidators).containsExactly(WholeFeedValidator.class); + } +} diff --git a/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoaderTest.java b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoaderTest.java index f299c2b53d..8607f56fcc 100644 --- a/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoaderTest.java +++ b/core/src/test/java/org/mobilitydata/gtfsvalidator/validator/ValidatorLoaderTest.java @@ -30,6 +30,7 @@ import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestFileValidator; import org.mobilitydata.gtfsvalidator.testgtfs.GtfsTestTableContainer; import org.mobilitydata.gtfsvalidator.testgtfs.WholeFeedValidator; +import org.mobilitydata.gtfsvalidator.validator.ValidatorLoader.ValidatorWithDependencyStatus; public class ValidatorLoaderTest { private static final CountryCode COUNTRY_CODE = CountryCode.forStringOrUnknown("AU"); @@ -68,15 +69,32 @@ public void createSingleFileValidator_injectsTableContainerAndContext() @Test public void createMultiFileValidator_injectsFeedContainerAndContext() throws ReflectiveOperationException { - GtfsTestTableContainer stopTable = new GtfsTestTableContainer(TableStatus.EMPTY_FILE); + GtfsTestTableContainer stopTable = + new GtfsTestTableContainer(TableStatus.PARSABLE_HEADERS_AND_ROWS); GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(stopTable)); - WholeFeedValidator validator = - (WholeFeedValidator) - ValidatorLoader.createMultiFileValidator( - WholeFeedValidator.class, feedContainer, VALIDATION_CONTEXT); + ValidatorWithDependencyStatus validatorWithStatus = + ValidatorLoader.createMultiFileValidator( + WholeFeedValidator.class, feedContainer, VALIDATION_CONTEXT); + assertThat(validatorWithStatus.dependenciesHaveErrors()).isFalse(); + + WholeFeedValidator validator = validatorWithStatus.validator(); assertThat(validator.getCountryCode()).isEqualTo(VALIDATION_CONTEXT.countryCode()); assertThat(validator.getCurrentDateTime()).isEqualTo(VALIDATION_CONTEXT.currentDateTime()); assertThat(validator.getFeedContainer()).isEqualTo(feedContainer); } + + @Test + public void createMultiFileValidator_singleContainer_dependenciesHaveErrors() + throws ReflectiveOperationException { + GtfsTestTableContainer table = new GtfsTestTableContainer(TableStatus.UNPARSABLE_ROWS); + GtfsFeedContainer feedContainer = new GtfsFeedContainer(ImmutableList.of(table)); + + ValidatorWithDependencyStatus validatorWithStatus = + ValidatorLoader.createMultiFileValidator( + GtfsTestFileValidator.class, feedContainer, VALIDATION_CONTEXT); + + assertThat(validatorWithStatus.dependenciesHaveErrors()).isTrue(); + assertThat(validatorWithStatus.validator().getStopTable()).isEqualTo(table); + } } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/runner/ValidationRunner.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/runner/ValidationRunner.java index 2d34577e97..d8ee01f0b0 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/runner/ValidationRunner.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/runner/ValidationRunner.java @@ -28,6 +28,8 @@ import java.time.Duration; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.List; +import java.util.stream.Collectors; import org.mobilitydata.gtfsvalidator.input.CurrentDateTime; import org.mobilitydata.gtfsvalidator.input.GtfsInput; import org.mobilitydata.gtfsvalidator.notice.IOError; @@ -127,7 +129,7 @@ public Status run(ValidationRunnerConfig config) { // Output exportReport(feedMetadata, noticeContainer, config, versionInfo); - printSummary(startNanos, feedContainer); + printSummary(startNanos, feedContainer, feedLoader); return Status.SUCCESS; } @@ -137,15 +139,23 @@ public Status run(ValidationRunnerConfig config) { * @param startNanos start time as nanoseconds * @param feedContainer the {@code GtfsFeedContainer} */ - public static void printSummary(long startNanos, GtfsFeedContainer feedContainer) { + public static void printSummary( + long startNanos, GtfsFeedContainer feedContainer, GtfsFeedLoader loader) { final long endNanos = System.nanoTime(); if (!feedContainer.isParsedSuccessfully()) { StringBuilder b = new StringBuilder(); + b.append("\n"); b.append(" ----------------------------------------- \n"); b.append("| !!! PARSING FAILED !!! |\n"); - b.append("| Most validators were never invoked. |\n"); + b.append("| Some validators were never invoked. |\n"); b.append("| Please see report.json for details. |\n"); b.append(" ----------------------------------------- \n"); + List> skippedValidators = loader.getSkippedValidators(); + if (!skippedValidators.isEmpty()) { + b.append("Skipped validators: "); + b.append( + skippedValidators.stream().map(Class::getSimpleName).collect(Collectors.joining(","))); + } logger.atSevere().log(b.toString()); } logger.atInfo().log("Validation took %.3f seconds%n", (endNanos - startNanos) / 1e9);