From 30d18320da55789ba9b9eed3496127a16b2b8084 Mon Sep 17 00:00:00 2001 From: Christoph Pirkl Date: Thu, 8 Feb 2024 10:09:22 +0100 Subject: [PATCH] #515 Parse changes file (#526) Co-authored-by: Christoph Kuhnke --- .../analyze/generic/MavenProcessBuilder.java | 1 - .../validators/VersionCollector.java | 6 +- .../changelog/ChangelogFileGenerator.java | 7 +- .../changelog/ChangelogFileValidator.java | 6 +- .../validators/changesfile/ChangesFile.java | 341 +++++++++++------- .../validators/changesfile/ChangesFileIO.java | 157 ++++++-- .../changesfile/ChangesFileName.java | 83 +++++ .../changesfile/ChangesFileSection.java | 114 ++++-- .../changesfile/ChangesFileValidator.java | 13 +- .../changesfile/DependencySectionFixer.java | 20 +- .../DependencyChangeReportRenderer.java | 18 +- .../files/LatestChangesFileValidator.java | 6 +- .../validators/VersionCollectorTest.java | 17 +- .../changelog/ChangelogFileGeneratorTest.java | 8 +- .../changesfile/ChangesFileIOTest.java | 146 +++++++- .../changesfile/ChangesFileNameTest.java | 12 + .../changesfile/ChangesFileSectionTest.java | 13 + .../changesfile/ChangesFileTest.java | 61 ++++ .../changesfile/ChangesFileValidatorTest.java | 31 +- .../DependencySectionFixerTest.java | 54 ++- .../DependencyChangeReportRendererTest.java | 28 +- .../files/LatestChangesFileValidatorTest.java | 6 +- 22 files changed, 846 insertions(+), 302 deletions(-) create mode 100644 project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileName.java create mode 100644 project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileNameTest.java create mode 100644 project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSectionTest.java create mode 100644 project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileTest.java diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/sources/analyze/generic/MavenProcessBuilder.java b/project-keeper/src/main/java/com/exasol/projectkeeper/sources/analyze/generic/MavenProcessBuilder.java index d7024931..b8252558 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/sources/analyze/generic/MavenProcessBuilder.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/sources/analyze/generic/MavenProcessBuilder.java @@ -7,7 +7,6 @@ import java.util.List; import com.exasol.projectkeeper.OsCheck; -import com.exasol.projectkeeper.OsCheck.OSType; /** * This class allows building and starting a {@code mvn} command. diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/VersionCollector.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/VersionCollector.java index e3030c8f..f5674c74 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/VersionCollector.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/VersionCollector.java @@ -8,7 +8,7 @@ import java.util.stream.Stream; import com.exasol.errorreporting.ExaError; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; /** * This class list all project-versions by scanning the doc/changes/ folder. @@ -30,10 +30,10 @@ public VersionCollector(final Path projectDirectory) { * * @return list of changes files */ - public List collectChangesFiles() { + public List collectChangesFiles() { try (final Stream filesStream = Files.walk(this.projectDirectory.resolve(Path.of("doc", "changes")))) { return filesStream // - .map(ChangesFile.Filename::from) // + .map(ChangesFileName::from) // .flatMap(Optional::stream) // .sorted(Comparator.reverseOrder()) // .collect(Collectors.toList()); diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGenerator.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGenerator.java index ea1a64f7..8e7ff839 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGenerator.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGenerator.java @@ -2,8 +2,7 @@ import java.util.List; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile.Filename; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; /** * This class generates the content for the changelog file. @@ -19,9 +18,9 @@ class ChangelogFileGenerator { */ final StringBuilder templateBuilder = new StringBuilder(); - String generate(final List filenames) { + String generate(final List filenames) { this.templateBuilder.append("# Changes" + NL + NL); - for (final Filename file : filenames) { + for (final ChangesFileName file : filenames) { this.templateBuilder.append("* [" + file.version() + "](" + file.filename() + ")" + NL); } return this.templateBuilder.toString(); diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileValidator.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileValidator.java index a6dc485c..111612d0 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileValidator.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileValidator.java @@ -8,14 +8,14 @@ import com.exasol.projectkeeper.Validator; import com.exasol.projectkeeper.validators.AbstractFileContentValidator; import com.exasol.projectkeeper.validators.VersionCollector; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; import com.exasol.projectkeeper.validators.finding.SimpleValidationFinding; import com.exasol.projectkeeper.validators.finding.ValidationFinding; /** * This is a {@link Validator} for the changelog files. */ -//[impl->dsn~verify-changelog-file~1] +// [impl->dsn~verify-changelog-file~1] public class ChangelogFileValidator extends AbstractFileContentValidator { private final Path projectDirectory; @@ -44,7 +44,7 @@ protected List validateContent(final String content) { @Override protected String getTemplate() { - final List versions = new VersionCollector(this.projectDirectory).collectChangesFiles(); + final List versions = new VersionCollector(this.projectDirectory).collectChangesFiles(); return new ChangelogFileGenerator().generate(versions); } } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFile.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFile.java index 38e3c4fd..c2e02c54 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFile.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFile.java @@ -1,105 +1,47 @@ package com.exasol.projectkeeper.validators.changesfile; import java.nio.file.Path; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import com.exasol.projectkeeper.mavenrepo.Version; +import com.exasol.errorreporting.ExaError; import com.vdurmont.semver4j.Semver; -import com.vdurmont.semver4j.Semver.SemverType; /** * This class represents a doc/changes/changes_x.x.x.md file. */ -public class ChangesFile { +public final class ChangesFile { /** Headline of the dependency updates section. */ public static final String DEPENDENCY_UPDATES_HEADING = "## Dependency Updates"; - private final List headerSectionLines; + /** Headline of the Summary section. */ + public static final String SUMMARY_HEADING = "## Summary"; + private final String projectName; + private final Semver projectVersion; + private final String releaseDate; + private final String codeName; + private final ChangesFileSection summarySection; private final List sections; + private final ChangesFileSection dependencyChangeSection; - /** - * Create a new instance of {@link ChangesFile}. - * - * @param headerLines lines of the changes file until the first level section - * @param sections sections of the changes file - */ - public ChangesFile(final List headerLines, final List sections) { - this.headerSectionLines = headerLines; - this.sections = sections; + private ChangesFile(final Builder builder) { + this.projectName = builder.projectName; + this.projectVersion = builder.projectVersion; + this.releaseDate = builder.releaseDate; + this.codeName = builder.codeName; + this.summarySection = builder.summarySection; + this.sections = List.copyOf(builder.sections); + this.dependencyChangeSection = builder.dependencyChangeSection; } /** - * Filename of a changes file, e.g. "changes_1.2.3.md". + * Get the relative path of the changes file for the given version. + * + * @param projectVersion project version + * @return relative path of the changes file, e.g. {@code doc/changes/changes_1.2.3.md} */ - public static class Filename implements Comparable { - /** Regular expression to identify valid names of changes files and to extract version number. **/ - public static final Pattern PATTERN = Pattern.compile("changes_(" + Version.PATTERN.pattern() + ")\\.md"); - - /** - * @param path path to create a {@link Filename} for - * @return If path matches regular expression for valid changes filenames then an {@link Optional} containing a - * new instance of {@link Filename}, otherwise {@code Optional.empty()}. - */ - public static Optional from(final Path path) { - final String filename = path.getFileName().toString(); - final Matcher matcher = PATTERN.matcher(filename); - if (!matcher.matches()) { - return Optional.empty(); - } - return Optional.of(new Filename(matcher.replaceFirst("$1"))); - } - - private final Semver version; - - /** - * Create a new instance of {@link ChangesFile.Filename}. - * - * @param version version to use for new instance - */ - public Filename(final String version) { - this.version = new Semver(version, SemverType.LOOSE); - } - - /** - * @return filename of the current {@link ChangesFile.Filename} as string - */ - public String filename() { - return "changes_" + this.version + ".md"; - } - - @Override - public int compareTo(final Filename o) { - return this.version.compareTo(o.version); - } - - /** - * @return version number contained in the filename of current {@link ChangesFile.Filename} - */ - public String version() { - return this.version.getValue(); - } - - @Override - public int hashCode() { - return Objects.hash(this.version); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Filename other = (Filename) obj; - return Objects.equals(this.version, other.version); - } + public static Path getPathForVersion(final String projectVersion) { + return Path.of("doc", "changes", new ChangesFileName(projectVersion).filename()); } /** @@ -112,24 +54,65 @@ public static Builder builder() { } /** - * Get the header of the changes section. - *

- * The header includes all lines until the first section {@code ##} starts. - *

- * - * @return list of lines of the header + * Get a builder configured with the this ChangesFile. This is useful for creating a copy and modify some parts of + * this object. + * + * @return a preconfigured builder */ - public List getHeaderSectionLines() { - return this.headerSectionLines; + public Builder toBuilder() { + return builder().projectName(this.projectName).projectVersion(this.projectVersion.toString()) + .releaseDate(this.releaseDate).codeName(this.codeName).summary(this.summarySection) + .sections(List.copyOf(this.sections)).dependencyChangeSection(this.dependencyChangeSection); } /** - * Get the heading of the file. - * - * @return heading (1. line) + * Get the project name for the first header line, e.g. {@code Project Keeper}. + * + * @return project name + */ + public String getProjectName() { + return projectName; + } + + /** + * Get the project version for the first header line, e.g. {@code 1.2.3}. + * + * @return project version + */ + public Semver getProjectVersion() { + return projectVersion; + } + + /** + * Get the release date for the first header line, e.g. {@code 2024-01-29} or {@code 2024-??-??}. + * + * @return release date + */ + public String getReleaseDate() { + return releaseDate; + } + + /** + * Get the code name of the release. + * + * @return code name */ - public String getHeading() { - return this.headerSectionLines.get(0); + public String getCodeName() { + return codeName; + } + + /** + * Get the parsed release date for the first header line. If the date is not valid (e.g. {@code 2024-??-??}), this + * will return an empty {@link Optional}. + * + * @return release date + */ + public Optional getParsedReleaseDate() { + try { + return Optional.of(LocalDate.parse(this.getReleaseDate())); + } catch (final DateTimeParseException exception) { + return Optional.empty(); + } } /** @@ -141,60 +124,175 @@ public List getSections() { return this.sections; } + /** + * Get the dependency change section. + * + * @return dependency change section + */ + public Optional getDependencyChangeSection() { + return Optional.ofNullable(this.dependencyChangeSection); + } + + /** + * Get the summary section. + * + * @return summary section + */ + public Optional getSummarySection() { + return Optional.ofNullable(this.summarySection); + } + @Override - public boolean equals(final Object other) { - if (this == other) { - return true; - } - if ((other == null) || (getClass() != other.getClass())) { - return false; - } - final ChangesFile that = (ChangesFile) other; - return Objects.equals(this.headerSectionLines, that.headerSectionLines) - && Objects.equals(this.sections, that.sections); + public String toString() { + return "ChangesFile [projectName=" + projectName + ", projectVersion=" + projectVersion + ", releaseDate=" + + releaseDate + ", codeName=" + codeName + ", summarySection=" + summarySection + ", sections=" + + sections + ", dependencyChangeSection=" + dependencyChangeSection + "]"; } @Override public int hashCode() { - return Objects.hash(this.headerSectionLines, this.sections); + return Objects.hash(projectName, projectVersion, releaseDate, codeName, summarySection, sections, + dependencyChangeSection); } @Override - public String toString() { - return String.join("\n", this.headerSectionLines) + "\n" - + this.sections.stream().map(ChangesFileSection::toString).collect(Collectors.joining("\n")); + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ChangesFile other = (ChangesFile) obj; + return Objects.equals(projectName, other.projectName) && Objects.equals(projectVersion, other.projectVersion) + && Objects.equals(releaseDate, other.releaseDate) && Objects.equals(codeName, other.codeName) + && Objects.equals(summarySection, other.summarySection) && Objects.equals(sections, other.sections) + && Objects.equals(dependencyChangeSection, other.dependencyChangeSection); } /** * Builder for {@link ChangesFile}. */ public static class Builder { - private final List sections = new ArrayList<>(); - private List header = Collections.emptyList(); + + private String projectName; + private Semver projectVersion; + private String releaseDate; + private String codeName; + private ChangesFileSection summarySection; + private List sections = new ArrayList<>(); + private ChangesFileSection dependencyChangeSection; private Builder() { // private constructor to hide public default } /** - * Set the header of the changes file. + * Set the project name for the first header line, e.g. {@code Project Keeper}. + * + * @param projectName project name + * @return self for fluent programming + */ + public Builder projectName(final String projectName) { + this.projectName = projectName; + return this; + } + + /** + * Set the project version for the first header line, e.g. {@code 1.2.3}. * - * @param header list of lines + * @param projectVersion project version * @return self for fluent programming */ - public Builder setHeader(final List header) { - this.header = header; + public Builder projectVersion(final String projectVersion) { + this.projectVersion = new Semver(projectVersion); + return this; + } + + /** + * Set the release date for the first header line, e.g. {@code 2024-01-29} or {@code 2024-??-??}. + * + * @param releaseDate release date + * @return self for fluent programming + */ + public Builder releaseDate(final String releaseDate) { + this.releaseDate = releaseDate; + return this; + } + + /** + * Set the code name of the release. + * + * @param codeName code name + * @return self for fluent programming + */ + public Builder codeName(final String codeName) { + this.codeName = codeName != null && codeName.isBlank() ? null : codeName; return this; } /** * Add a section to the changes file. * - * @param lines list of lines + * @param section section * @return self for fluent programming */ - public Builder addSection(final List lines) { - this.sections.add(new ChangesFileSection(lines)); + public Builder addSection(final ChangesFileSection section) { + this.sections.add(section); + return this; + } + + /** + * Set the {@code Summary} section for the changes file. + * + * @param section section + * @return self for fluent programming + */ + public Builder summary(final ChangesFileSection section) { + if (section == null) { + this.summarySection = null; + return this; + } + if (!section.getHeading().equals(SUMMARY_HEADING)) { + throw new IllegalArgumentException(ExaError.messageBuilder("E-PK-CORE-178").message( + "Dependency change section has invalid heading {{heading}}, expected {{expected heading}}", + section.getHeading(), SUMMARY_HEADING).ticketMitigation().toString()); + } + this.summarySection = section; + return this; + } + + /** + * Set all sections of the changes file. + * + * @param sections list of sections + * @return self for fluent programming + */ + public Builder sections(final List sections) { + this.sections = List.copyOf(sections); + return this; + } + + /** + * Add a an optional {@code Dependency Updates} section to the changes file. + * + * @param section section + * @return self for fluent programming + */ + public Builder dependencyChangeSection(final ChangesFileSection section) { + if (section == null) { + this.dependencyChangeSection = null; + return this; + } + if (!section.getHeading().equals(DEPENDENCY_UPDATES_HEADING)) { + throw new IllegalArgumentException(ExaError.messageBuilder("E-PK-CORE-179").message( + "Dependency change section has invalid heading {{heading}}, expected {{expected heading}}", + section.getHeading(), DEPENDENCY_UPDATES_HEADING).ticketMitigation().toString()); + } + this.dependencyChangeSection = section; return this; } @@ -204,7 +302,8 @@ public Builder addSection(final List lines) { * @return built {@link ChangesFile} */ public ChangesFile build() { - return new ChangesFile(this.header, this.sections); + return new ChangesFile(this); } + } } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIO.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIO.java index 3853c6f2..a34765f0 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIO.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIO.java @@ -2,18 +2,24 @@ import java.io.*; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; import java.util.regex.Pattern; import com.exasol.errorreporting.ExaError; +import com.exasol.projectkeeper.validators.changesfile.ChangesFile.Builder; /** * This class reads and writes a {@link ChangesFile} from disk. */ public class ChangesFileIO { - private static final Pattern SECTION_HEADING_PATTERN = Pattern.compile("\\s*##\\s.*"); - private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String CODE_NAME = "Code name:"; + private static final String PROJECT_NAME_PATTERN = "[\\w\\s-]+"; + private static final String VERSION_PATTERN = "\\d+\\.\\d+\\.\\d+"; + private static final String DATE_PATTERN = "\\d{4}-[\\d?]{2}-[\\d?]{2}"; + private static final Pattern FIRST_LINE_PATTERN = Pattern + .compile("^# (" + PROJECT_NAME_PATTERN + ") (" + VERSION_PATTERN + "), released (" + DATE_PATTERN + ")$"); + private static final String LINE_SEPARATOR = "\n"; /** * Read a {@link ChangesFile} from disk. @@ -23,19 +29,7 @@ public class ChangesFileIO { */ public ChangesFile read(final Path file) { try (final var fileReader = new BufferedReader(new FileReader(file.toFile()))) { - String sectionHeader = null; - String line; - final var builder = ChangesFile.builder(); - final List lineBuffer = new ArrayList<>(); - while ((line = fileReader.readLine()) != null) { - if (SECTION_HEADING_PATTERN.matcher(line).matches()) { - makeSection(sectionHeader, builder, lineBuffer); - sectionHeader = line; - } - lineBuffer.add(line); - } - makeSection(sectionHeader, builder, lineBuffer); - return builder.build(); + return read(file, fileReader); } catch (final IOException exception) { throw new IllegalStateException(ExaError.messageBuilder("F-PK-CORE-39") .message("Failed to read changes file {{file}}.").parameter("file", file.toString()).toString(), @@ -43,15 +37,83 @@ public ChangesFile read(final Path file) { } } - private void makeSection(final String sectionHeader, final ChangesFile.Builder builder, - final List lineBuffer) { - if (!lineBuffer.isEmpty()) { - if (sectionHeader == null) { - builder.setHeader(List.copyOf(lineBuffer)); - } else { - builder.addSection(List.copyOf(lineBuffer)); + ChangesFile read(final Path file, final BufferedReader fileReader) throws IOException { + return new Parser(file, fileReader).parse(); + } + + private static class Parser { + final Path file; + final BufferedReader reader; + final Builder builder = ChangesFile.builder(); + ChangesFileSection.Builder currentSection; + int lineCount = 0; + + Parser(final Path file, final BufferedReader reader) { + this.file = file; + this.reader = reader; + } + + ChangesFile parse() throws IOException { + String line; + while ((line = reader.readLine()) != null) { + parseLine(line); + lineCount++; + } + addSection(); + return builder.build(); + } + + private void parseLine(final String line) { + if (lineCount == 0) { + parseFirstLine(file, line); + return; + } + if (line.startsWith(CODE_NAME)) { + builder.codeName(line.substring(CODE_NAME.length()).trim()); + return; + } + if (line.startsWith("## ")) { + addSection(); + currentSection = ChangesFileSection.builder(line); + return; + } + if (currentSection != null) { + currentSection.addLine(line); } - lineBuffer.clear(); + } + + private void addSection() { + if (currentSection == null) { + return; + } + final ChangesFileSection section = currentSection.build(); + currentSection = null; + switch (section.getHeading()) { + case ChangesFile.SUMMARY_HEADING: + builder.summary(section); + break; + case ChangesFile.DEPENDENCY_UPDATES_HEADING: + builder.dependencyChangeSection(section); + break; + default: + builder.addSection(section); + break; + } + } + + private void parseFirstLine(final Path filePath, final String line) { + final Matcher matcher = FIRST_LINE_PATTERN.matcher(line); + if (!matcher.matches()) { + throw new IllegalStateException(ExaError.messageBuilder("E-PK-CORE-171") + .message("Changes file {{file path}} contains invalid first line {{first line}}.", filePath, + line) + .mitigation("Update first line so that it matches regex {{expected regular expression}}", + FIRST_LINE_PATTERN) + .toString()); + } + builder.projectName(matcher.group(1)) // + .projectVersion(matcher.group(2)) // + .releaseDate(matcher.group(3)); } } @@ -62,12 +124,8 @@ private void makeSection(final String sectionHeader, final ChangesFile.Builder b * @param destinationFile file to write to */ public void write(final ChangesFile changesFile, final Path destinationFile) { - try (final var fileWriter = new BufferedWriter(new FileWriter(destinationFile.toFile()))) { - writeSection(fileWriter, changesFile.getHeaderSectionLines()); - for (final ChangesFileSection section : changesFile.getSections()) { - writeSection(fileWriter, section.getContent()); - } - fileWriter.flush(); + try (final var writer = new BufferedWriter(new FileWriter(destinationFile.toFile()))) { + write(changesFile, writer); } catch (final IOException exception) { throw new IllegalStateException( ExaError.messageBuilder("E-PK-CORE-41").message("Failed to write changes file {{file name}}.") @@ -76,10 +134,39 @@ public void write(final ChangesFile changesFile, final Path destinationFile) { } } - private void writeSection(final BufferedWriter fileWriter, final List content) throws IOException { - for (final String line : content) { - fileWriter.write(line); - fileWriter.write(LINE_SEPARATOR); + void write(final ChangesFile changesFile, final Writer writer) throws IOException { + writeHeader(writer, changesFile); + for (final ChangesFileSection section : changesFile.getSections()) { + writeSection(writer, section); + } + final Optional dependencyChangeSection = changesFile.getDependencyChangeSection(); + if (dependencyChangeSection.isPresent()) { + writer.write(dependencyChangeSection.get().toString()); + writer.write(LINE_SEPARATOR); + } + } + + private void writeHeader(final Writer writer, final ChangesFile changesFile) throws IOException { + writer.write("# " + changesFile.getProjectName() + " " + changesFile.getProjectVersion() + ", released " + + changesFile.getReleaseDate()); + writer.write(LINE_SEPARATOR); + writer.write(LINE_SEPARATOR); + writer.write(CODE_NAME + (changesFile.getCodeName() != null ? " " + changesFile.getCodeName() : "")); + writer.write(LINE_SEPARATOR); + writer.write(LINE_SEPARATOR); + final Optional summarySection = changesFile.getSummarySection(); + if (summarySection.isPresent()) { + writer.write(summarySection.get().toString()); + writer.write(LINE_SEPARATOR); + } + } + + private void writeSection(final Writer writer, final ChangesFileSection section) throws IOException { + writer.write(section.getHeading()); + writer.write(LINE_SEPARATOR); + for (final String line : section.getContent()) { + writer.write(line); + writer.write(LINE_SEPARATOR); } } } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileName.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileName.java new file mode 100644 index 00000000..390c6df5 --- /dev/null +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileName.java @@ -0,0 +1,83 @@ +package com.exasol.projectkeeper.validators.changesfile; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.exasol.projectkeeper.mavenrepo.Version; +import com.vdurmont.semver4j.Semver; +import com.vdurmont.semver4j.Semver.SemverType; + +/** + * Filename of a changes file, e.g. {@code changes_1.2.3.md}. + */ +public final class ChangesFileName implements Comparable { + /** Regular expression to identify valid names of changes files and to extract version number. **/ + public static final Pattern PATTERN = Pattern.compile("changes_(" + Version.PATTERN.pattern() + ")\\.md"); + + /** + * @param path path to create a {@link ChangesFileName} for + * @return If path matches regular expression for valid changes filenames then an {@link Optional} containing a new + * instance of {@link ChangesFileName}, otherwise {@code Optional.empty()}. + */ + public static Optional from(final Path path) { + final String filename = path.getFileName().toString(); + final Matcher matcher = PATTERN.matcher(filename); + if (!matcher.matches()) { + return Optional.empty(); + } + return Optional.of(new ChangesFileName(matcher.replaceFirst("$1"))); + } + + private final Semver version; + + /** + * Create a new instance of {@link ChangesFileName}. + * + * @param version version to use for new instance + */ + public ChangesFileName(final String version) { + this.version = new Semver(version, SemverType.LOOSE); + } + + /** + * @return filename of the current {@link ChangesFileName} as string + */ + public String filename() { + return "changes_" + this.version + ".md"; + } + + @Override + public int compareTo(final ChangesFileName o) { + return this.version.compareTo(o.version); + } + + /** + * @return version number contained in the filename of current {@link ChangesFileName} + */ + public String version() { + return this.version.getValue(); + } + + @Override + public int hashCode() { + return Objects.hash(this.version); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ChangesFileName other = (ChangesFileName) obj; + return Objects.equals(this.version, other.version); + } +} diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSection.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSection.java index b5109bf9..5e5a1ac1 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSection.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSection.java @@ -1,9 +1,8 @@ package com.exasol.projectkeeper.validators.changesfile; -import java.util.List; -import java.util.Objects; +import static java.util.Arrays.asList; -import com.exasol.errorreporting.ExaError; +import java.util.*; /** * Section of a {@link ChangesFile}. @@ -11,20 +10,14 @@ * Each level two heading (##) starts a new section. *

*/ -public class ChangesFileSection { +public final class ChangesFileSection { + + private final String heading; private final List content; - /** - * Create a new instance of {@link ChangesFileSection}. - * - * @param content lines - */ - public ChangesFileSection(final List content) { - if (content.isEmpty()) { - throw new IllegalStateException(ExaError.messageBuilder("F-PK-CORE-36") - .message("changes file sections must not be empty.").ticketMitigation().toString()); - } - this.content = content; + private ChangesFileSection(final Builder builder) { + this.heading = Objects.requireNonNull(builder.heading, "header"); + this.content = List.copyOf(builder.lines); } /** @@ -33,7 +26,7 @@ public ChangesFileSection(final List content) { * @return heading */ public String getHeading() { - return this.content.get(0); + return this.heading; } /** @@ -46,22 +39,91 @@ public List getContent() { } @Override - public boolean equals(final Object other) { - if (this == other) - return true; - if (other == null || getClass() != other.getClass()) - return false; - final ChangesFileSection that = (ChangesFileSection) other; - return Objects.equals(this.content, that.content); + public int hashCode() { + return Objects.hash(heading, content); } @Override - public int hashCode() { - return Objects.hash(this.content); + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ChangesFileSection other = (ChangesFileSection) obj; + return Objects.equals(heading, other.heading) && Objects.equals(content, other.content); } @Override public String toString() { - return String.join("\n", this.content); + return heading + "\n" + String.join("\n", this.content); + } + + /** + * Create a new {@link Builder} for creating a {@link ChangesFileSection}. + * + * @param heading the heading for the new section + * @return a new builder + */ + public static Builder builder(final String heading) { + return new Builder(heading); + } + + /** + * A builder for creating {@link ChangesFileSection}s. + */ + public static class Builder { + private final String heading; + private final List lines = new ArrayList<>(); + + private Builder(final String heading) { + this.heading = heading; + } + + /** + * Add the given lines to the content of the new {@code ChangesFileSection}. + * + * @param lines lines to add + * @return {@code this} for fluent programming + */ + public Builder addLines(final String... lines) { + this.lines.addAll(asList(lines)); + return this; + } + + /** + * Add the given lines to the content of the new {@code ChangesFileSection}. + * + * @param lines lines to add + * @return {@code this} for fluent programming + */ + public Builder addLines(final List lines) { + this.lines.addAll(lines); + return this; + } + + /** + * Add the given line to the content of the new {@code ChangesFileSection}. + * + * @param line line to add + * @return {@code this} for fluent programming + */ + public Builder addLine(final String line) { + this.lines.add(line); + return this; + } + + /** + * Build a new {@link ChangesFileSection}. + * + * @return a new {@link ChangesFileSection}. + */ + public ChangesFileSection build() { + return new ChangesFileSection(this); + } } } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidator.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidator.java index 30a8bb5b..ba21db83 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidator.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidator.java @@ -31,7 +31,7 @@ public class ChangesFileValidator extends AbstractFileValidator { */ public ChangesFileValidator(final String projectVersion, final String projectName, final Path projectDirectory, final List sources) { - super(projectDirectory, Path.of("doc", "changes", new ChangesFile.Filename(projectVersion).filename())); + super(projectDirectory, ChangesFile.getPathForVersion(projectVersion)); this.projectVersion = projectVersion; this.projectName = projectName; this.sources = sources; @@ -72,10 +72,13 @@ private ChangesFile fixSections(final ChangesFile changesFile) { } private ChangesFile getTemplate() { - final var changesFile = ChangesFile.builder() - .setHeader(List.of("# " + this.projectName + " " + this.projectVersion + ", released " - + LocalDateTime.now().getYear() + "-??-??", "", "Code name:", "")) // - .addSection(List.of("## Summary", "", "## Features", "", "* ISSUE_NUMBER: description", "")) // + final String releaseDate = LocalDateTime.now().getYear() + "-??-??"; + final var changesFile = ChangesFile.builder().projectName(this.projectName).projectVersion(this.projectVersion) + .releaseDate(releaseDate) // + .codeName("") // + .summary(ChangesFileSection.builder("## Summary").build()) + .addSection(ChangesFileSection.builder("## Features").addLines("", "* ISSUE_NUMBER: description", "") + .build()) // .build(); return fixSections(changesFile); } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixer.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixer.java index 01a08241..9db14cf0 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixer.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixer.java @@ -1,9 +1,7 @@ package com.exasol.projectkeeper.validators.changesfile; -import static com.exasol.projectkeeper.validators.changesfile.ChangesFile.DEPENDENCY_UPDATES_HEADING; - -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import com.exasol.projectkeeper.sources.AnalyzedSource; @@ -12,7 +10,7 @@ /** * This class fixes the dependency section of a {@link ChangesFile}. */ -//[impl->dsn~dependency-section-in-changes_x.x.x.md-file-validator~1] +// [impl->dsn~dependency-section-in-changes_x.x.x.md-file-validator~1] class DependencySectionFixer { private final List sources; @@ -35,20 +33,12 @@ public DependencySectionFixer(final List sources) { public ChangesFile fix(final ChangesFile changesFile) { final List reports = this.sources.stream().map(this::getDependencyChangesOfSource) .collect(Collectors.toList()); - final List renderedReport = new DependencyChangeReportRenderer().render(reports); - final List sections = new ArrayList<>(changesFile.getSections()); - removeDependencySection(sections); - if (!renderedReport.isEmpty()) { - sections.add(new ChangesFileSection(renderedReport)); - } - return new ChangesFile(List.copyOf(changesFile.getHeaderSectionLines()), sections); + final Optional dependencyChanges = new DependencyChangeReportRenderer().render(reports); + return changesFile.toBuilder() // + .dependencyChangeSection(dependencyChanges.orElse(null)).build(); } private NamedDependencyChangeReport getDependencyChangesOfSource(final AnalyzedSource source) { return new NamedDependencyChangeReport(source.getProjectName(), source.getDependencyChanges()); } - - private void removeDependencySection(final List sections) { - sections.removeIf(section -> section.getHeading().compareToIgnoreCase(DEPENDENCY_UPDATES_HEADING) == 0); - } } diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRenderer.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRenderer.java index aca0d23d..34695e34 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRenderer.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRenderer.java @@ -2,14 +2,12 @@ import static com.exasol.projectkeeper.ApStyleFormatter.capitalizeApStyle; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import com.exasol.projectkeeper.shared.dependencies.BaseDependency.Type; import com.exasol.projectkeeper.shared.dependencychanges.DependencyChange; import com.exasol.projectkeeper.shared.dependencychanges.DependencyChangeReport; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; -import com.exasol.projectkeeper.validators.changesfile.NamedDependencyChangeReport; +import com.exasol.projectkeeper.validators.changesfile.*; /** * String renderer for {@link DependencyChangeReport}. @@ -20,17 +18,15 @@ public class DependencyChangeReportRenderer { * Render a {@link DependencyChangeReport} to string. * * @param reports reports to render - * @return rendered report as a list of lines + * @return rendered report as a section */ - public List render(final List reports) { + public Optional render(final List reports) { final List content = renderContent(reports); - final List lines = new ArrayList<>(); if (content.isEmpty()) { - return lines; + return Optional.empty(); } - lines.add(ChangesFile.DEPENDENCY_UPDATES_HEADING); - lines.addAll(content); - return lines; + return Optional.of(ChangesFileSection.builder(ChangesFile.DEPENDENCY_UPDATES_HEADING) // + .addLines(content).build()); } private List renderContent(final List reports) { diff --git a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidator.java b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidator.java index afd5cb05..86236087 100644 --- a/project-keeper/src/main/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidator.java +++ b/project-keeper/src/main/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidator.java @@ -7,7 +7,7 @@ import com.exasol.errorreporting.ExaError; import com.exasol.projectkeeper.Validator; import com.exasol.projectkeeper.validators.VersionCollector; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; import com.exasol.projectkeeper.validators.finding.SimpleValidationFinding; import com.exasol.projectkeeper.validators.finding.ValidationFinding; @@ -32,11 +32,11 @@ public LatestChangesFileValidator(final Path projectDir, final String projectVer @Override public List validate() { final List empty = Collections.emptyList(); - final List list = new VersionCollector(this.projectDirectory).collectChangesFiles(); + final List list = new VersionCollector(this.projectDirectory).collectChangesFiles(); if (list.isEmpty()) { return empty; } - final ChangesFile.Filename latest = list.get(0); + final ChangesFileName latest = list.get(0); if (latest.version().equals(this.projectVersion)) { return empty; } diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/VersionCollectorTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/VersionCollectorTest.java index 56c91af0..0f178b0f 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/VersionCollectorTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/VersionCollectorTest.java @@ -13,8 +13,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile.Filename; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; class VersionCollectorTest { @Test @@ -29,20 +28,20 @@ void sorted(@TempDir final Path tempDir) throws IOException { "1.0.0")) { createChangesFile(folder, version); } - final List expected = Stream.of( // + final List expected = Stream.of( // "1.1.0", // "1.0.10", // "1.0.2", // "1.0.0", // "0.3.0") // - .map(ChangesFile.Filename::new) // + .map(ChangesFileName::new) // .collect(Collectors.toList()); assertThat(new VersionCollector(tempDir).collectChangesFiles(), equalTo(expected)); } - private ChangesFile.Filename createChangesFile(final Path folder, final String version) throws IOException { - final Filename cfile = new Filename(version); - Files.createFile(folder.resolve(cfile.filename())); - return cfile; + private ChangesFileName createChangesFile(final Path folder, final String version) throws IOException { + final ChangesFileName file = new ChangesFileName(version); + Files.createFile(folder.resolve(file.filename())); + return file; } -} \ No newline at end of file +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGeneratorTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGeneratorTest.java index 58a45393..3f66983a 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGeneratorTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changelog/ChangelogFileGeneratorTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; class ChangelogFileGeneratorTest { @Test @@ -27,7 +27,7 @@ void testNonStandardVersionFormats(final String version) { assertThat(new ChangelogFileGenerator().generate(files(version)), containsString(version)); } - private List files(final String... versions) { - return Arrays.stream(versions).map(ChangesFile.Filename::new).collect(Collectors.toList()); + private List files(final String... versions) { + return Arrays.stream(versions).map(ChangesFileName::new).collect(Collectors.toList()); } -} \ No newline at end of file +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIOTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIOTest.java index 191984a6..3f7748c1 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIOTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileIOTest.java @@ -1,12 +1,13 @@ package com.exasol.projectkeeper.validators.changesfile; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.time.LocalDate; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -24,37 +25,142 @@ void testParsing() throws IOException { final ChangesFile changesFile = new ChangesFileIO().read(changesFilePath); final List headings = changesFile.getSections().stream().map(ChangesFileSection::getHeading) .collect(Collectors.toList()); - assertThat(changesFile.getHeading(), equalTo("# My Project 0.1.0, released 1980-01-01")); - assertThat(headings, contains("## Summary", "## Features", "## Bug Fixes", "## Documentation", "## Refactoring", - "## Dependency Updates")); + assertThat(changesFile.getProjectName(), equalTo("My Project")); + assertThat(changesFile.getProjectVersion().toString(), equalTo("0.1.0")); + assertThat(changesFile.getReleaseDate(), equalTo("1980-01-01")); + assertThat(changesFile.getParsedReleaseDate().get(), equalTo(LocalDate.parse("1980-01-01"))); + assertThat(changesFile.getSummarySection().get().getContent(), contains("", "My summary", "")); + assertThat(headings, contains("## Features", "## Bug Fixes", "## Documentation", "## Refactoring")); + assertThat(changesFile.getDependencyChangeSection().get().getHeading(), equalTo("## Dependency Updates")); + assertThat(changesFile.getDependencyChangeSection().get().getContent(), hasSize(18)); } @Test void testWriting() throws IOException { - final ChangesFile changesFile = ChangesFile.builder().setHeader(List.of("# MyChanges")) - .addSection(List.of("## My Subsection")).build(); + final ChangesFile changesFile = ChangesFile.builder().projectName("project").projectVersion("1.2.3") + .releaseDate("2023-??-??").codeName("my code name") + .summary(ChangesFileSection.builder("## Summary").addLine("my summary content").build()) + .addSection(ChangesFileSection.builder("# MyChanges").build()) + .addSection(ChangesFileSection.builder("## My Subsection").addLine("content").build()).build(); final Path testFile = this.tempDir.resolve("myFile.md"); new ChangesFileIO().write(changesFile, testFile); - assertThat(Files.readString(testFile), - equalTo("# MyChanges" + System.lineSeparator() + "## My Subsection" + System.lineSeparator())); + assertThat(Files.readString(testFile), equalTo( + "# project 1.2.3, released 2023-??-??\n\nCode name: my code name\n\n## Summary\nmy summary content\n# MyChanges\n## My Subsection\ncontent\n")); } @Test void testReadAndWrite() throws IOException { - final Path changesFilePath = loadExampleFileToTempDir(); - final ChangesFileIO changesFileIO = new ChangesFileIO(); - final ChangesFile changesFile = changesFileIO.read(changesFilePath); - final Path testFile = this.tempDir.resolve("result.md"); - changesFileIO.write(changesFile, testFile); - assertThat(Files.readString(testFile), equalTo(Files.readString(changesFilePath))); + final String content = readExampleFile(); + assertReadWrite(content); + } + + @Test + void testReadInvalidFirstLineFails() { + final IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> readFromString("# invalid first line")); + assertThat(exception.getMessage(), startsWith( + "E-PK-CORE-171: Changes file 'dummy-file' contains invalid first line '# invalid first line'. Update first line so that it matches regex")); + } + + @Test + void testReadFirstLineWithDummyReleaseDate() throws IOException { + final ChangesFile changesFile = readFromString( + "# Project Name 1.2.3, released 2024-??-??\nCode name: my code name\n## Summary\n\n"); + assertThat(changesFile.getProjectName(), equalTo("Project Name")); + assertThat(changesFile.getProjectVersion().toString(), equalTo("1.2.3")); + assertThat(changesFile.getReleaseDate(), equalTo("2024-??-??")); + assertThat(changesFile.getCodeName(), equalTo("my code name")); + assertThat(changesFile.getParsedReleaseDate().isPresent(), is(false)); + assertWriteRead(changesFile); + } + + @Test + void testReadFirstLineWithValidReleaseDate() throws IOException { + final ChangesFile changesFile = readFromString("# Project Name 1.2.3, released 2024-01-29\n## Summary\n\n"); + assertThat(changesFile.getProjectName(), equalTo("Project Name")); + assertThat(changesFile.getProjectVersion().toString(), equalTo("1.2.3")); + assertThat(changesFile.getReleaseDate(), equalTo("2024-01-29")); + assertThat(changesFile.getParsedReleaseDate().get(), equalTo(LocalDate.parse("2024-01-29"))); + assertWriteRead(changesFile); + } + + @Test + void testReadMissingSummary() throws IOException { + final ChangesFile changesFile = readFromString("# Project Name 1.2.3, released 2024-01-29"); + assertThat(changesFile.getSummarySection().isEmpty(), is(true)); + } + + @Test + void testReadEmptySummary() throws IOException { + final ChangesFile changesFile = readFromString("# Project Name 1.2.3, released 2024-01-29\n## Summary"); + final ChangesFileSection summary = changesFile.getSummarySection().get(); + assertThat(summary.getHeading(), equalTo("## Summary")); + assertThat(summary.getContent(), emptyIterable()); + } + + @Test + void testReadSummary() throws IOException { + final ChangesFile changesFile = readFromString( + "# Project Name 1.2.3, released 2024-01-29\n## Summary\nmy\ncontent\n"); + final ChangesFileSection summary = changesFile.getSummarySection().get(); + assertThat(summary.getHeading(), equalTo("## Summary")); + assertThat(summary.getContent(), contains("my", "content")); + assertWriteRead(changesFile); + } + + @Test + void testReadNoDependencySection() throws IOException { + final ChangesFile changesFile = readFromString("# Project Name 1.2.3, released 2024-01-29\n## Summary"); + assertThat(changesFile.getDependencyChangeSection().isEmpty(), is(true)); + } + + @Test + void testReadDependencySection() throws IOException { + final ChangesFile changesFile = readFromString( + "# Project Name 1.2.3, released 2024-01-29\n## Summary\n## Dependency Updates\nmy\ncontent"); + assertThat(changesFile.getDependencyChangeSection().isEmpty(), is(false)); + assertThat(changesFile.getDependencyChangeSection().get().getHeading(), equalTo("## Dependency Updates")); + assertThat(changesFile.getDependencyChangeSection().get().getContent(), contains("my", "content")); } private Path loadExampleFileToTempDir() throws IOException { final Path changesFile = this.tempDir.resolve("changed_0.1.0.md"); - try (final InputStream exampleFileStream = getClass().getClassLoader() - .getResourceAsStream("changesFileExample1.md")) { + try (final InputStream exampleFileStream = getExampleFileStream()) { Files.copy(Objects.requireNonNull(exampleFileStream), changesFile, StandardCopyOption.REPLACE_EXISTING); } return changesFile; } -} \ No newline at end of file + + private String readExampleFile() throws IOException { + try (final InputStream exampleFileStream = getExampleFileStream()) { + return new String(exampleFileStream.readAllBytes(), StandardCharsets.UTF_8).replace("\r\n", "\n"); + } + } + + private InputStream getExampleFileStream() { + return getClass().getClassLoader().getResourceAsStream("changesFileExample1.md"); + } + + private void assertWriteRead(final ChangesFile changesFile) throws IOException { + final String content = writeToString(changesFile); + final ChangesFile readChangesFile = readFromString(content); + assertThat(readChangesFile.toString(), equalTo(changesFile.toString())); + assertThat(readChangesFile, equalTo(changesFile)); + } + + private void assertReadWrite(final String content) throws IOException { + final ChangesFile changesFile = readFromString(content); + final String writtenContent = writeToString(changesFile); + assertThat(writtenContent, equalTo(content)); + } + + private String writeToString(final ChangesFile changesFile) throws IOException { + final StringWriter stringWriter = new StringWriter(); + new ChangesFileIO().write(changesFile, stringWriter); + return stringWriter.toString(); + } + + private ChangesFile readFromString(final String content) throws IOException { + return new ChangesFileIO().read(Path.of("dummy-file"), new BufferedReader(new StringReader(content))); + } +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileNameTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileNameTest.java new file mode 100644 index 00000000..d1a792ee --- /dev/null +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileNameTest.java @@ -0,0 +1,12 @@ +package com.exasol.projectkeeper.validators.changesfile; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class ChangesFileNameTest { + @Test + void equalsContractFilename() { + EqualsVerifier.forClass(ChangesFileName.class).verify(); + } +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSectionTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSectionTest.java new file mode 100644 index 00000000..929afba6 --- /dev/null +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileSectionTest.java @@ -0,0 +1,13 @@ +package com.exasol.projectkeeper.validators.changesfile; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class ChangesFileSectionTest { + + @Test + void equalsContract() { + EqualsVerifier.forClass(ChangesFileSection.class).verify(); + } +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileTest.java new file mode 100644 index 00000000..d4b3ab20 --- /dev/null +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileTest.java @@ -0,0 +1,61 @@ +package com.exasol.projectkeeper.validators.changesfile; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import java.nio.file.Path; +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import com.exasol.projectkeeper.validators.changesfile.ChangesFile.Builder; +import com.jparams.verifier.tostring.ToStringVerifier; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class ChangesFileTest { + + @Test + void equalsContract() { + EqualsVerifier.forClass(ChangesFile.class).verify(); + } + + @Test + void testToString() { + ToStringVerifier.forClass(ChangesFile.class).verify(); + } + + @Test + void getPathForVersion() { + assertThat(ChangesFile.getPathForVersion("1.2.3"), equalTo(Path.of("doc/changes/changes_1.2.3.md"))); + } + + @Test + void toBuilderCreatesCopy() { + final ChangesFile changesFile = builder().build(); + final ChangesFile copy = changesFile.toBuilder().build(); + assertThat(copy, equalTo(changesFile)); + assertThat(changesFile.equals(copy), is(true)); + } + + private Builder builder() { + return ChangesFile.builder().projectName("name").projectVersion("1.2.3").releaseDate("2023-??-??") + .codeName("my code name") + .summary(ChangesFileSection.builder("## Summary").addLine("summary content").build()) + .dependencyChangeSection(ChangesFileSection.builder("## Dependency Updates") + .addLine("dependency update content").build()) + .addSection(ChangesFileSection.builder("section 1").build()); + } + + @Test + void getParsedReleaseDateValid() { + assertThat(builder().releaseDate("2024-01-29").build().getParsedReleaseDate().get(), + equalTo(LocalDate.of(2024, 1, 29))); + } + + @Test + void getParsedReleaseDateInvalid() { + assertThat(builder().releaseDate("invalid").build().getParsedReleaseDate().isPresent(), is(false)); + } +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidatorTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidatorTest.java index 9cf9bf31..62234975 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidatorTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/ChangesFileValidatorTest.java @@ -4,10 +4,8 @@ import static com.exasol.projectkeeper.HasNoMoreFindingsAfterApplyingFixesMatcher.hasNoMoreFindingsAfterApplyingFixes; import static com.exasol.projectkeeper.HasValidationFindingWithMessageMatcher.hasNoValidationFindings; import static com.exasol.projectkeeper.HasValidationFindingWithMessageMatcher.hasValidationFindingWithMessage; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -35,6 +33,7 @@ class ChangesFileValidatorTest { private static final String A_VERSION = "1.2.3"; private static final String A_PROJECT_NAME = "my-project"; + private static final String LINE_SEPARATOR = "\n"; @TempDir Path tempDir; @@ -55,12 +54,18 @@ void testValidationForSnapshotVersion() throws IOException { } @Test - void noDepdendencyUpdates() throws IOException { + void noDependencyUpdates() throws IOException { final AnalyzedMavenSource source = createTestSetup(new TestMavenModel(), Collections.emptyList()); final Logger log = mock(Logger.class); createValidator(source).validate().forEach(finding -> new FindingsFixer(log).fixFindings(List.of(finding))); final Path changesFile = this.tempDir.resolve(Path.of("doc", "changes", "changes_1.2.3.md")); - assertThat(changesFile, hasContent(startsWith("# my-project 1.2.3, release"))); + assertThat(changesFile, + hasContent(equalTo("# my-project 1.2.3, released 2024-??-??" + LINE_SEPARATOR + LINE_SEPARATOR // + + "Code name:" + LINE_SEPARATOR + LINE_SEPARATOR // + + "## Summary" + LINE_SEPARATOR + LINE_SEPARATOR // + + "## Features" + LINE_SEPARATOR + LINE_SEPARATOR // + + "* ISSUE_NUMBER: description" + LINE_SEPARATOR + LINE_SEPARATOR))); + verify(log).info("Created 'doc" + File.separator + "changes" + File.separator + "changes_1.2.3.md'. Don't forget to update its content!"); final List findings = createValidator(source).validate(); @@ -73,7 +78,15 @@ void testFixCreatedTemplate() throws IOException { final Logger log = mock(Logger.class); createValidator(source).validate().forEach(finding -> new FindingsFixer(log).fixFindings(List.of(finding))); final Path changesFile = this.tempDir.resolve(Path.of("doc", "changes", "changes_1.2.3.md")); - assertThat(changesFile, hasContent(startsWith("# my-project 1.2.3, release"))); + assertThat(changesFile, + hasContent(equalTo("# my-project 1.2.3, released 2024-??-??" + LINE_SEPARATOR + LINE_SEPARATOR // + + "Code name:" + LINE_SEPARATOR + LINE_SEPARATOR // + + "## Summary" + LINE_SEPARATOR + LINE_SEPARATOR // + + "## Features" + LINE_SEPARATOR + LINE_SEPARATOR // + + "* ISSUE_NUMBER: description" + LINE_SEPARATOR + LINE_SEPARATOR // + + "## Dependency Updates" + LINE_SEPARATOR + LINE_SEPARATOR // + + "### Compile Dependency Updates" + LINE_SEPARATOR + LINE_SEPARATOR + + "* Added `com.example:my-lib:1.2.3`" + LINE_SEPARATOR))); verify(log).info("Created 'doc" + File.separator + "changes" + File.separator + "changes_1.2.3.md'. Don't forget to update its content!"); } @@ -85,7 +98,9 @@ void testFixContainsDependencyUpdates() throws IOException { final AnalyzedMavenSource source = createTestSetup(model); createValidator(source).validate().forEach(FindingFixHelper::fix); final Path changesFile = this.tempDir.resolve(Path.of("doc", "changes", "changes_1.2.3.md")); - assertThat(changesFile, hasContent(containsString("my-lib"))); + assertThat(changesFile, hasContent(endsWith("## Dependency Updates" + LINE_SEPARATOR + LINE_SEPARATOR // + + "### Compile Dependency Updates" + LINE_SEPARATOR + LINE_SEPARATOR // + + "* Added `com.example:my-lib:1.2.3`" + LINE_SEPARATOR))); } @Test @@ -117,4 +132,4 @@ private AnalyzedMavenSource createTestSetup(final TestMavenModel mavenModel, .dependencyChanges(DependencyChangeReport.builder().typed(Type.COMPILE, dependencyChanges).build()) // .build(); } -} \ No newline at end of file +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixerTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixerTest.java index 90ca108e..d02f8e7f 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixerTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/DependencySectionFixerTest.java @@ -2,8 +2,7 @@ import static com.exasol.projectkeeper.validators.changesfile.ChangesFile.DEPENDENCY_UPDATES_HEADING; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.*; import java.util.List; @@ -13,9 +12,10 @@ import com.exasol.projectkeeper.shared.dependencychanges.DependencyChangeReport; import com.exasol.projectkeeper.shared.dependencychanges.NewDependency; import com.exasol.projectkeeper.sources.AnalyzedMavenSource; +import com.exasol.projectkeeper.validators.changesfile.ChangesFile.Builder; @Tag("integration") -//[utest->dsn~dependency-section-in-changes_x.x.x.md-file-validator~1] +// [utest->dsn~dependency-section-in-changes_x.x.x.md-file-validator~1] class DependencySectionFixerTest { private static AnalyzedMavenSource source; @@ -32,29 +32,43 @@ static void beforeAll() { @Test void testSectionIsAdded() { - final ChangesFile changesFile = ChangesFile.builder().setHeader(List.of("heading")).build(); - final List sections = new DependencySectionFixer(List.of(source)).fix(changesFile) - .getSections(); - assertThat(sections.size(), equalTo(1)); - assertThat(sections.get(0).getHeading(), equalTo(DEPENDENCY_UPDATES_HEADING)); + final ChangesFile changesFile = changesFileBuilder().addSection(ChangesFileSection.builder("heading").build()) + .build(); + final ChangesFile fixedChangesFile = new DependencySectionFixer(List.of(source)).fix(changesFile); + assertThat(fixedChangesFile.getDependencyChangeSection().get().getHeading(), + equalTo(DEPENDENCY_UPDATES_HEADING)); + } + + private Builder changesFileBuilder() { + return ChangesFile.builder().projectName("projectName").projectVersion("1.2.3").releaseDate("releaseDate") + .summary(ChangesFileSection.builder("## Summary").build()); } @Test void testSectionIsUpdated() { - final ChangesFile changesFile = ChangesFile.builder().setHeader(List.of("heading")) - .addSection(List.of(DEPENDENCY_UPDATES_HEADING, "myLine")).build(); + final ChangesFile changesFile = changesFileBuilder().dependencyChangeSection( + ChangesFileSection.builder("## Dependency Updates").addLine("content will be overwritten").build()) + .build(); final ChangesFile fixedChangesFile = new DependencySectionFixer(List.of(source)).fix(changesFile); - final List sections = fixedChangesFile.getSections(); - assertThat(sections.size(), equalTo(1)); - assertThat(sections.get(0).getHeading(), equalTo(DEPENDENCY_UPDATES_HEADING)); - assertThat("dependency fixer changed the changes file", changesFile, not(equalTo(fixedChangesFile))); + final ChangesFileSection changesFileSection = fixedChangesFile.getDependencyChangeSection().get(); + assertThat(changesFileSection.getHeading(), equalTo(DEPENDENCY_UPDATES_HEADING)); + assertThat(changesFileSection.getContent(), + contains("", "### Compile Dependency Updates", "", "* Added `com.example:my-lib:1.2.3`")); + assertThat("dependency fixer changed the changes file", changesFile, + allOf(not(equalTo(fixedChangesFile)), not(sameInstance(fixedChangesFile)))); } @Test - void testHeaderIsPreserved() { - final ChangesFile changesFile = ChangesFile.builder().setHeader(List.of("heading")) - .addSection(List.of(DEPENDENCY_UPDATES_HEADING, "myLine")).build(); - final ChangesFile fixedChangesFile = new DependencySectionFixer(List.of(source)).fix(changesFile); - assertThat(changesFile.getHeaderSectionLines(), equalTo(fixedChangesFile.getHeaderSectionLines())); + void testDependencySectionIsRemoved() { + final ChangesFile changesFile = changesFileBuilder().addSection(ChangesFileSection.builder("heading").build()) + .dependencyChangeSection(ChangesFileSection.builder("## Dependency Updates") + .addLine("content will be preserved").build()) + .build(); + final ChangesFile fixedChangesFile = new DependencySectionFixer(List.of()).fix(changesFile); + + assertThat(fixedChangesFile.getDependencyChangeSection().isPresent(), is(false)); + assertThat(fixedChangesFile, not(sameInstance(changesFile))); + assertThat("dependency fixer changed the changes file", changesFile, + allOf(not(equalTo(fixedChangesFile)), not(sameInstance(fixedChangesFile)))); } -} \ No newline at end of file +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRendererTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRendererTest.java index 6ad5cde4..12eb1942 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRendererTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/changesfile/dependencies/DependencyChangeReportRendererTest.java @@ -1,16 +1,19 @@ package com.exasol.projectkeeper.validators.changesfile.dependencies; +import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; import com.exasol.projectkeeper.shared.dependencies.BaseDependency.Type; import com.exasol.projectkeeper.shared.dependencychanges.DependencyChangeReport; import com.exasol.projectkeeper.shared.dependencychanges.NewDependency; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileSection; import com.exasol.projectkeeper.validators.changesfile.NamedDependencyChangeReport; class DependencyChangeReportRendererTest { @@ -22,17 +25,15 @@ class DependencyChangeReportRendererTest { @Test void testRenderSingleSourceReport() { final NamedDependencyChangeReport namedReport = new NamedDependencyChangeReport("my-project", REPORT); - final String result = String.join("\n", new DependencyChangeReportRenderer().render(List.of(namedReport))); - assertThat(result, equalTo("## Dependency Updates\n" + "\n" + "### Compile Dependency Updates\n" + "\n" - + "* Added `com.example:my-lib:1.2.3`")); + assertThat(render(namedReport), equalTo("## Dependency Updates\n" + "\n" + "### Compile Dependency Updates\n" + + "\n" + "* Added `com.example:my-lib:1.2.3`")); } @Test void testRenderMultiSourceReport() { final NamedDependencyChangeReport sourceA = new NamedDependencyChangeReport("project A", REPORT); final NamedDependencyChangeReport sourceB = new NamedDependencyChangeReport("project B", REPORT); - final String result = String.join("\n", new DependencyChangeReportRenderer().render(List.of(sourceA, sourceB))); - assertThat(result, equalTo( + assertThat(render(sourceA, sourceB), equalTo( "## Dependency Updates\n\n### Project A\n\n#### Compile Dependency Updates\n\n* Added `com.example:my-lib:1.2.3`\n\n### Project B\n\n#### Compile Dependency Updates\n\n* Added `com.example:my-lib:1.2.3`")); } @@ -40,15 +41,20 @@ void testRenderMultiSourceReport() { void testRenderMultiSourceReportWithNoChangesInOneReport() { final NamedDependencyChangeReport sourceA = new NamedDependencyChangeReport("project A", REPORT); final NamedDependencyChangeReport sourceB = new NamedDependencyChangeReport("project B", EMPTY_REPORT); - final String result = String.join("\n", new DependencyChangeReportRenderer().render(List.of(sourceA, sourceB))); - assertThat(result, equalTo( + assertThat(render(sourceA, sourceB), equalTo( "## Dependency Updates\n\n### Project A\n\n#### Compile Dependency Updates\n\n* Added `com.example:my-lib:1.2.3`")); } @Test void testRenderSourceReportWithoutChanges() { final NamedDependencyChangeReport sourceA = new NamedDependencyChangeReport("project A", EMPTY_REPORT); - final String result = String.join("\n", new DependencyChangeReportRenderer().render(List.of(sourceA))); - assertThat(result, emptyString()); + final Optional result = new DependencyChangeReportRenderer().render(List.of(sourceA)); + assertThat(result.isPresent(), is(false)); } -} \ No newline at end of file + + private String render(final NamedDependencyChangeReport... reports) { + final Optional section = new DependencyChangeReportRenderer().render(asList(reports)); + assertThat(section.isPresent(), is(true)); + return section.get().toString(); + } +} diff --git a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidatorTest.java b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidatorTest.java index a030479a..64ba39c5 100644 --- a/project-keeper/src/test/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidatorTest.java +++ b/project-keeper/src/test/java/com/exasol/projectkeeper/validators/files/LatestChangesFileValidatorTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import com.exasol.projectkeeper.validators.changesfile.ChangesFile; +import com.exasol.projectkeeper.validators.changesfile.ChangesFileName; import com.exasol.projectkeeper.validators.finding.SimpleValidationFinding; class LatestChangesFileValidatorTest { @@ -38,8 +38,8 @@ private LatestChangesFileValidator testee(final Path tempDir, final String... ve final Path folder = tempDir.resolve(Path.of("doc", "changes")); Files.createDirectories(folder); for (final String v : versions) { - final ChangesFile.Filename cfile = new ChangesFile.Filename(v); - Files.createFile(folder.resolve(cfile.filename())); + final ChangesFileName file = new ChangesFileName(v); + Files.createFile(folder.resolve(file.filename())); } return new LatestChangesFileValidator(tempDir, "2.0.0"); }