All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.creekservice.internal.system.test.parser.YamlTestPackageParser Maven / Gradle / Ivy

There is a newer version: 0.4.1
Show newest version
/*
 * Copyright 2022-2023 Creek Contributors (https://github.com/creek-service)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.creekservice.internal.system.test.parser;

import static java.lang.System.lineSeparator;
import static java.util.Objects.requireNonNull;
import static org.creekservice.api.system.test.model.TestPackage.testPackage;

import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.creekservice.api.base.type.Suppliers;
import org.creekservice.api.system.test.extension.test.model.Expectation;
import org.creekservice.api.system.test.extension.test.model.Input;
import org.creekservice.api.system.test.extension.test.model.Ref;
import org.creekservice.api.system.test.model.TestCase;
import org.creekservice.api.system.test.model.TestCaseDef;
import org.creekservice.api.system.test.model.TestPackage;
import org.creekservice.api.system.test.model.TestSuite;
import org.creekservice.api.system.test.model.TestSuiteDef;
import org.creekservice.api.system.test.parser.ModelType;
import org.creekservice.api.system.test.parser.TestPackageParser;

/**
 * Parse test suites from a directory structure of Yaml files.
 *
 * 

Expected directory structure: * *

 * root
 *   |--seed
 *   |--inputs
 *   |--expectations
 * 
* *

...with test suites defined in the root directory. */ public final class YamlTestPackageParser implements TestPackageParser { private static final PathMatcher YAML_MATCHER = FileSystems.getDefault().getPathMatcher("regex:.*\\.yml|.*\\.yaml"); private static final Path SEED = Paths.get("seed"); private static final Path INPUTS = Paths.get("inputs"); private static final Path EXPECTATIONS = Paths.get("expectations"); private final ObjectMapper mapper; private final Observer observer; /** * @param modelExtensions known model extensions * @param observer a parsing observer */ public YamlTestPackageParser( final Collection> modelExtensions, final Observer observer) { this.mapper = SystemTestMapper.create(modelExtensions); this.observer = requireNonNull(observer, "observer"); } @Override public Optional parse(final Path path, final Predicate predicate) { if (!Files.isDirectory(path.resolve(EXPECTATIONS))) { // Test cases must have at least one expectation. // Therefore, no expectation dir means no test suites: return Optional.empty(); } final List seedData = loadDir(path.resolve(SEED), Input.class) .map(LazyFile::content) .collect(Collectors.toList()); final Map> inputs = loadDir(path.resolve(INPUTS), Input.class) .collect(Collectors.toMap(LazyFile::id, Function.identity())); final Map> expectations = loadDir(path.resolve(EXPECTATIONS), Expectation.class) .collect(Collectors.toMap(LazyFile::id, Function.identity())); final List suites = loadDir(path, TestSuiteDef.class) .filter(f -> predicate.test(f.path())) .map(f -> testSuiteBuilder(f.content(), inputs, expectations)) .collect(Collectors.toUnmodifiableList()); if (suites.isEmpty()) { return Optional.empty(); } warnOnUnusedDependencies(path, inputs, expectations); return Optional.of(testPackage(seedData, suites)); } private Stream> loadDir(final Path dir, final Class type) { return ymlFilesInDir(dir).stream() .map(path -> new LazyFile<>(id(path), path, () -> parse(path, type))); } @SuppressFBWarnings( value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", justification = "false negative") private List ymlFilesInDir(final Path dir) { if (!Files.exists(dir)) { return List.of(); } try (Stream stream = Files.walk(dir, 1)) { return stream.filter(Files::isRegularFile) .filter(YAML_MATCHER::matches) .collect(Collectors.toUnmodifiableList()); } catch (final IOException e) { throw new TestLoadFailedException("Error accessing directory " + dir, e); } } private T parse(final Path path, final Class type) { try { return mapper.readValue(path.toFile(), type); } catch (final Exception e) { throw new InvalidTestFileException( "Failed to load " + type.getSimpleName() + " from " + path.toUri() + lineSeparator() + "Please check the file is valid." + lineSeparator() + e.getMessage(), e); } } private static TestSuite.Builder testSuiteBuilder( final TestSuiteDef def, final Map> inputs, final Map> expectations) { try { final List testCases = def.tests().stream() .map(testCaseDef -> testCaseBuilder(testCaseDef, inputs, expectations)) .collect(Collectors.toList()); return TestSuite.testSuite(testCases, def); } catch (final InvalidTestFileException | MissingDependencyException e) { throw new InvalidTestFileException( "Error in suite '" + def.name() + "':" + e.getMessage(), e); } } private static TestCase.Builder testCaseBuilder( final TestCaseDef def, final Map> inputs, final Map> expectations) { try { final List testInputs = def.inputs().stream() .map(i -> findDependency(i, inputs)) .collect(Collectors.toList()); final List testExpectations = def.expectations().stream() .map(e -> findDependency(e, expectations)) .collect(Collectors.toList()); return TestCase.testCase(testInputs, testExpectations, def); } catch (final InvalidTestFileException | MissingDependencyException e) { throw new InvalidTestFileException("'" + def.name() + "': " + e.getMessage(), e); } } private void warnOnUnusedDependencies( final Path path, final Map> inputs, final Map> expectations) { final List unused = Stream.concat(inputs.values().stream(), expectations.values().stream()) .filter(LazyFile::unused) .map(LazyFile::path) .map(Path::toAbsolutePath) .collect(Collectors.toUnmodifiableList()); if (!unused.isEmpty()) { observer.unusedDependencies(path, unused); } } private static T findDependency(final Ref ref, final Map> known) { final LazyFile dependency = known.get(ref.id()); if (dependency == null) { throw new MissingDependencyException(ref); } return dependency.content(); } @SuppressFBWarnings( value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "path always has at least one part and extension") private static String id(final Path path) { final String fileName = path.getFileName().toString(); return fileName.substring(0, fileName.lastIndexOf('.')); } private static final class LazyFile { private final String id; private final Path path; private final Supplier content; private boolean unused = true; LazyFile(final String id, final Path path, final Supplier content) { this.id = requireNonNull(id, "id"); this.path = requireNonNull(path, "path"); this.content = Suppliers.memoize(content); } String id() { return id; } Path path() { return path; } T content() { unused = false; return content.get(); } boolean unused() { return unused; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy