package net.sourceforge.plantuml; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import net.sourceforge.plantuml.error.PSystemError; class PipeTest { ByteArrayOutputStream baos; ErrorStatus errorStatus; Option option; Pipe pipe; PrintStream ps; @BeforeEach void setup() { PSystemError.disableTimeBasedErrorDecorations(); errorStatus = ErrorStatus.init(); baos = new ByteArrayOutputStream(); option = new Option(); ps = new PrintStream(baos); pipe = new Pipe(option, ps, new ByteArrayInputStream(new byte[0]), UTF_8.toString()); } @Test void should_managePipe_set_as_no_error_for_empty_input() throws IOException { pipe = new Pipe(option, ps, new ByteArrayInputStream(new byte[0]), UTF_8.name()); pipe.managePipe(errorStatus); try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { softly.assertThat(errorStatus.hasError()).isFalse(); softly.assertThat(errorStatus.isNoData()).isTrue(); softly.assertThat(baos.toByteArray()).isEmpty(); } } static List managePipeTestCases() { LinkedList l = new LinkedList<>(); l.add(TestCase.of("", "", "", Verification.EXACT, false, true)); // ok render l.add(TestCase.of("", "a->b", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, false, false)); l.add(TestCase.of("", "@startuml\na->b\n@enduml", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, false, false)); l.add(TestCase.of("", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n", "(?s).*(PNG.*Generated by https://plantuml.com.*){2}", Verification.REGEX, false, false)); // bad data l.add(TestCase.of("", "a", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, true, false)); l.add(TestCase.of("", "@startuml\na\n@enduml\n", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, true, false)); l.add(TestCase.of("", "@startuml\na\n@enduml\n@startuml\na\n@enduml\n", "(?s).*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, true, false)); // ignore garbage before; after; before&between&after diagrams l.add(TestCase.of("", "this-is-garbage\n@startuml\na->b\n@enduml", "(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, false, false)); l.add(TestCase.of("", "@startuml\na->b\n@enduml\nthis-is-garbage", "(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, false, false)); l.add(TestCase.of("", "this-is-garbage\n@startuml\na->b\n@enduml\nthis-is-garbage\n@startuml\na->b\n@enduml\nthis-is-garbage", "(?s).*(PNG.*Generated by https://plantuml.com.*){2}", Verification.REGEX, false, false)); // ignore other diagram start tags when still not closed (but fails to generate) l.add(TestCase.of("", "@startuml\na->b\n@startgantt\n@enduml\n", "(?s).*(PNG.*Generated by https://plantuml.com.*){1}", Verification.REGEX, true, false)); // manage @@@format svg l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml", "(?s).*<\\?xml.*", Verification.REGEX, false, false)); l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\n@@@format svg\na->b\n@enduml", "(?s).*(<\\?xml.*.*){2}", Verification.REGEX, false, false)); // mixed formats l.add(TestCase.of("", "@startuml\n@@@format png\na->b\n@enduml\n@startuml\n@@@format svg\na->b\n@enduml", "(?s).*PNG.*Generated by https://plantuml.com.*<\\?xml.*", Verification.REGEX, false, false)); l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\n@@@format png\na->b\n@enduml", "(?s).*<\\?xml.*.*PNG.*Generated by https://plantuml.com.*", Verification.REGEX, false, false)); // pipe delimitor l.add(TestCase.of("-pipedelimitor PIPE-DELIMITOR", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n", "(?s).*(PNG.*Generated by https://plantuml.com.*PIPE-DELIMITOR.*){2}", Verification.REGEX, false, false)); // if format is set in first diagram and not in the second, it will be used in // both (Possibly incorrect: preseved from old behaviour) l.add(TestCase.of("", "@startuml\n@@@format svg\na->b\n@enduml\n@startuml\na->b\n@enduml", "(?s).*(<\\?xml.*.*){2}", Verification.REGEX, false, false)); // ok computeurl l.add(TestCase.of("-computeurl", "@startuml\na->b\n@enduml", "IzIrIm80\n", Verification.EXACT, false, true)); l.add(TestCase.of("-computeurl", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n", "IzIrIm80\nIzIrI-9AqhLB1W00\n", Verification.EXACT, false, true)); // ok encodeurl l.add(TestCase.of("-encodeurl", "@startuml\na->b\n@enduml", "IzIrIm80\n", Verification.EXACT, false, true)); l.add(TestCase.of("-encodeurl", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n", "IzIrIm80\nIzIrI-9AqhLB1W00\n", Verification.EXACT, false, true)); // valid syntax l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml", "SEQUENCE\n(2 participants)\n", Verification.EXACT, false, false)); l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml\n@startuml\na->b\nb->c\n@enduml\n", "SEQUENCE\n(2 participants)\nSEQUENCE\n(3 participants)\n", Verification.EXACT, false, false)); l.add(TestCase.of("-syntax", "@startgantt\n[a] lasts 1 day\n@endgantt", "OTHER\n(Gantt)\n", Verification.EXACT, false, false)); // invalid syntax l.add(TestCase.of("-syntax", "@startuml\na\n@enduml", "ERROR\n1\nSyntax Error?\n", Verification.EXACT, true, false)); l.add(TestCase.of("-syntax", "@startuml\na\n@enduml\n@startuml\na\n@enduml", "ERROR\n1\nSyntax Error?\nERROR\n1\nSyntax Error?\n", Verification.EXACT, true, false)); l.add(TestCase.of("-syntax", "@startuml\na->b\n@enduml\n@startuml\na\n@enduml", "SEQUENCE\n(2 participants)\nERROR\n1\nSyntax Error?\n", Verification.EXACT, true, false)); l.add(TestCase.of("-syntax", "@startuml\na\n@enduml\n@startuml\na->b\n@enduml", "ERROR\n1\nSyntax Error?\nSEQUENCE\n(2 participants)\n", Verification.EXACT, true, false)); // pipemap (using regexp to allow any coords so that it doesn't fail on // different systems) l.add(TestCase.of("-pipemap", "@startuml\na->b: [[http://a.com]] c\n@enduml", "\n" + "\"\"\n" + "\n\n", Verification.REGEX, false, false)); l.add(TestCase.of("-pipemap", "@startuml\na->b: [[http://a.com]] c\n@enduml\n@startuml\nc->d: [[http://c.com]] e\n@enduml", "\n" + "\"\"\n" + "\n\n" + "\n" + "\"\"\n" + "\n\n", Verification.REGEX, false, false)); // no links/invalid input => no pipemap to output (no error as of // https://forum.plantuml.net/10049/2019-pipemap-diagrams-containing-links-give-zero-exit-code // ) l.add(TestCase.of("-pipemap", "@startuml\na->b\n@enduml", "\n", Verification.EXACT, false, false)); l.add(TestCase.of("-pipemap", "@startuml\na\n@enduml", "\n", Verification.EXACT, false, false)); return l; } @ParameterizedTest @MethodSource("managePipeTestCases") void should_managePipe_manage_success_cases_correctly(TestCase testCase) throws IOException, InterruptedException { option = new Option(testCase.getOptions().split(" ")); pipe = new Pipe(option, ps, new ByteArrayInputStream(testCase.getInput().getBytes(UTF_8)), UTF_8.name()); pipe.managePipe(errorStatus); try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { softly.assertThat(errorStatus.hasError()).isEqualTo(testCase.isExpectedHasErrors()); softly.assertThat(errorStatus.isNoData()).isEqualTo(testCase.isExpectedIsNoData()); testCase.getExpectedOutVerification().assertOk(softly, baos, testCase.getExpectedOut()); } } @Test void should_readFirstDiagram_return_null_for_empty_input() throws IOException { pipe = new Pipe(option, null, new ByteArrayInputStream(new byte[0]), UTF_8.name()); String actual = pipe.readFirstDiagram(); assertThat(actual).isNull(); } @Test void should_readFirstDiagram_decode_a_special_unicode_character_when_provided_charset_is_utf8() throws IOException { pipe = new Pipe(option, null, new ByteArrayInputStream("\u2620\n".getBytes(UTF_8)), UTF_8.name()); String actual = pipe.readFirstDiagram(); assertThat(actual).isEqualTo("@startuml\n\u2620\n@enduml\n"); } // The testing is relevant only if the testing VM is configured in non-UTF-8 // defaultCharset() @Test void should_readFirstDiagram_uses_current_charset_if_not_provided() throws IOException { String input = "\u00C1"; // A acute accented. (HTML Á). Multibyte in UTF-8. if (Charset.defaultCharset().newEncoder().canEncode(input)) { pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(Charset.defaultCharset())), null); String actual = pipe.readFirstDiagram(); assertThat(actual).isEqualTo("@startuml\n\u00C1\n@enduml\n"); } else { // default charset can't encode Á. Ignore the test. assertTrue(true); } } @ParameterizedTest @ValueSource(strings = { "ab\nc", // *nix, macOsX "ab\rc", // pre-macOsX macs "ab\r\nc", // Windows // the case \n\r is handled as 2 new lines, thus not added "ab\nc\n", "ab\nc\r", "ab\nc\r\n" }) void should_readFirstDiagram_decode_correctly_different_line_endings(String input) throws IOException { pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.name()); String first = pipe.readFirstDiagram(); String second = pipe.readFirstDiagram(); assertThat(first).isEqualTo("@startuml\nab\nc\n@enduml\n"); assertThat(second).isEqualTo(null); // no spurious diagram afterwards } static List firstStartAndEndMarks() { List l = new LinkedList<>(); l.add(InputExpected.of("\nab\r\ncde", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("\nab\r\ncde\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n@enduml\n")); l.add(InputExpected.of("\nab\r\ncde\n@endwhatever", "@startuml\nab\ncde\n@endwhatever\n@enduml\n")); l.add(InputExpected.of("\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("this-is-garbage\n@startuml\nab\rcde\n@enduml\nthis-is-garbage\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startwhatever\nab\rcde\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever\n")); l.add(InputExpected.of("@start\nab\rcde\n@end", "@start\nab\ncde\n@end\n")); l.add(InputExpected.of("@startX\nab\rcde\n@endX", "@startX\nab\ncde\n@endX\n")); l.add(InputExpected.of("@startXY\nab\rcde\n@endXY", "@startXY\nab\ncde\n@endXY\n")); l.add(InputExpected.of("@startXYZ\nab\rcde\n@endXYZ", "@startXYZ\nab\ncde\n@endXYZ\n")); l.add(InputExpected.of("@start1\nab\rcde\n@end1", "@start1\nab\ncde\n@end1\n")); l.add(InputExpected.of("@start12\nab\rcde\n@end12", "@start12\nab\ncde\n@end12\n")); l.add(InputExpected.of("@start123\nab\rcde\n@end123", "@start123\nab\ncde\n@end123\n")); return l; } @ParameterizedTest @MethodSource("firstStartAndEndMarks") void should_readFirstDiagram_handle_correctly_start_and_end_marks(InputExpected inputExpected) throws IOException { pipe = new Pipe(option, null, new ByteArrayInputStream(inputExpected.getInput().getBytes(UTF_8)), UTF_8.name()); String actual = pipe.readFirstDiagram(); assertThat(actual).isEqualTo(inputExpected.getExpected()); } static List subsequentStartAndEndMarks() { List l = new LinkedList<>(); l.add(InputExpected.of("\nab\r\ncde", null)); l.add(InputExpected.of("\nab\r\ncde\n", null)); l.add(InputExpected.of("\nab\r\ncde\n@enduml", null)); l.add(InputExpected.of("\nab\r\ncde\n@endwhatever", null)); l.add(InputExpected.of("\nab\r\ncde\n@enduml\n", null)); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\n\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startuml\nab\r\ncde\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("this-is-garbage\n@startuml\nab\rcde\n@enduml\nthis-is-garbage\n", "@startuml\nab\ncde\n@enduml\n")); l.add(InputExpected.of("@startwhatever\nab\rcde\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever\n")); l.add(InputExpected.of("@start\nab\rcde\n@end", "@start\nab\ncde\n@end\n")); l.add(InputExpected.of("@startX\nab\rcde\n@endX", "@startX\nab\ncde\n@endX\n")); l.add(InputExpected.of("@startXY\nab\rcde\n@endXY", "@startXY\nab\ncde\n@endXY\n")); l.add(InputExpected.of("@startXYZ\nab\rcde\n@endXYZ", "@startXYZ\nab\ncde\n@endXYZ\n")); l.add(InputExpected.of("@start1\nab\rcde\n@end1", "@start1\nab\ncde\n@end1\n")); l.add(InputExpected.of("@start12\nab\rcde\n@end12", "@start12\nab\ncde\n@end12\n")); l.add(InputExpected.of("@start123\nab\rcde\n@end123", "@start123\nab\ncde\n@end123\n")); return l; } @ParameterizedTest @MethodSource("subsequentStartAndEndMarks") void should_readSubsequentDiagram_handle_correctly_start_and_end_marks(InputExpected inputExpected) throws IOException { pipe = new Pipe(option, null, new ByteArrayInputStream(inputExpected.getInput().getBytes(UTF_8)), UTF_8.name()); String actual = pipe.readSubsequentDiagram(); assertThat(actual).isEqualTo(inputExpected.getExpected()); } @ParameterizedTest @ValueSource(strings = { "@@@format png", "png", "@@@format png " }) void should_manageFormat_handle_png(String valid) { pipe.manageFormat(valid); assertThat(option.getFileFormatOption().getFileFormat()).isEqualTo(FileFormat.PNG); } @ParameterizedTest @ValueSource(strings = { "@@@format svg", "svg", "@@@format svg " }) void should_manageFormat_handle_svg(String valid) { pipe.manageFormat(valid); assertThat(option.getFileFormatOption().getFileFormat()).isEqualTo(FileFormat.SVG); } @ParameterizedTest @ValueSource(strings = { "@@@format invalid", "invalid", "@@@format invalid " }) void should_manageFormat_ignore_any_other_value_and_keep_default_value_as_file_format_option(String valid) { pipe.manageFormat(valid); assertThat(option.getFileFormatOption().getFileFormat()).isEqualTo(FileFormat.PNG); } static class TestCase { private final String options; private final String input; private final String expectedOut; private final Verification expectedOutVerification; private final boolean expectedHasErrors; private final boolean expectedIsNoData; public TestCase(String options, String input, String expectedOut, Verification expectedOutVerification, boolean expectedHasErrors, boolean expectedIsNoData) { this.options = options; this.input = input; this.expectedOut = expectedOut; this.expectedOutVerification = expectedOutVerification; this.expectedHasErrors = expectedHasErrors; this.expectedIsNoData = expectedIsNoData; } public static TestCase of(String option, String input, String expectedOut, Verification expectedOutVerification, boolean expectedHasErrors, boolean expectedIsNoData) { return new TestCase(option, input, expectedOut, expectedOutVerification, expectedHasErrors, expectedIsNoData); } public String getOptions() { return options; } public String getInput() { return input; } public String getExpectedOut() { return expectedOut; } public Verification getExpectedOutVerification() { return expectedOutVerification; } public boolean isExpectedHasErrors() { return expectedHasErrors; } public boolean isExpectedIsNoData() { return expectedIsNoData; } @Override public String toString() { return "o:'" + options + "', i:'" + input + "', e-out='" + expectedOut + "', e-err='" + expectedHasErrors + "', e-nodata='" + expectedIsNoData + "'"; } } static class InputExpected { private final String input; private final String expected; public InputExpected(String input, String expected) { this.input = input; this.expected = expected; } public static InputExpected of(String input, String expected) { return new InputExpected(input, expected); } public String getInput() { return input; } public String getExpected() { return expected; } @Override public String toString() { return "i:'" + input + "', e='" + expected + "'"; } } enum Verification { EXACT, REGEX; void assertOk(AutoCloseableSoftAssertions softly, ByteArrayOutputStream baos, String expectedOut) { final String result = new String(baos.toByteArray(), UTF_8).replaceAll("\r", ""); switch (this) { case EXACT: softly.assertThat(result).isEqualTo(expectedOut); break; case REGEX: softly.assertThat(result).matches(expectedOut); break; } } } }