From b3677c66f29013f724386ecb9f4650d3fe7e4666 Mon Sep 17 00:00:00 2001 From: Josep Mones Teixidor Date: Thu, 29 Jul 2021 03:48:02 +0200 Subject: [PATCH 1/3] Ignore .java-version for jenv users --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b1ff38f29..a3f4d5091 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ target/ .settings/ .classpath .project + +# for jenv users +.java-version From 6849c96144221ed0e09004703f452fb1f4fce338 Mon Sep 17 00:00:00 2001 From: Josep Mones Teixidor Date: Thu, 29 Jul 2021 03:50:30 +0200 Subject: [PATCH 2/3] Add tests for Pipe --- src/net/sourceforge/plantuml/Pipe.java | 6 +- test/net/sourceforge/plantuml/PipeTest.java | 353 ++++++++++++++++++++ 2 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 test/net/sourceforge/plantuml/PipeTest.java diff --git a/src/net/sourceforge/plantuml/Pipe.java b/src/net/sourceforge/plantuml/Pipe.java index ffe271519..a27ec41e6 100644 --- a/src/net/sourceforge/plantuml/Pipe.java +++ b/src/net/sourceforge/plantuml/Pipe.java @@ -150,7 +150,7 @@ public class Pipe { return s == null || s.startsWith("@end"); } - private String readOneDiagram() throws IOException { + String readOneDiagram() throws IOException { final StringBuilder sb = new StringBuilder(); while (true) { final String s = readOneLine(); @@ -176,7 +176,7 @@ public class Pipe { return source; } - private void manageFormat(String s) { + void manageFormat(String s) { if (s.contains("png")) { option.setFileFormatOption(new FileFormatOption(FileFormat.PNG)); } else if (s.contains("svg")) { @@ -184,7 +184,7 @@ public class Pipe { } } - private String readOneLine() throws IOException { + String readOneLine() throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { final int read = is.read(); diff --git a/test/net/sourceforge/plantuml/PipeTest.java b/test/net/sourceforge/plantuml/PipeTest.java new file mode 100644 index 000000000..57c4ea01a --- /dev/null +++ b/test/net/sourceforge/plantuml/PipeTest.java @@ -0,0 +1,353 @@ +package net.sourceforge.plantuml; + + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +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; + + +class PipeTest { + + ByteArrayOutputStream baos; + ErrorStatus errorStatus; + Option option; + Pipe pipe; + PrintStream ps; + + @BeforeEach + void setup() { + 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 http://plantuml.com.*", Verification.REGEX, false, false)); + l.add(TestCase.of("", "@startuml\na->b\n@enduml", "(?s).*PNG.*Generated by http://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 http://plantuml.com.*){2}", Verification.REGEX, false, false)); + + // bad data + l.add(TestCase.of("", "a", "(?s).*PNG.*Generated by http://plantuml.com.*", Verification.REGEX, true, false)); + l.add(TestCase.of("", "@startuml\na\n@enduml\n", "(?s).*PNG.*Generated by http://plantuml.com.*", Verification.REGEX, true, false)); + l.add(TestCase.of("", "@startuml\na\n@enduml\n@startuml\na\n@enduml\n", "(?s).*PNG.*Generated by http://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 http://plantuml.com.*){1}", Verification.REGEX, false, false)); + l.add(TestCase.of("", "@startuml\na->b\n@enduml\nthis-is-garbage", "(?s).*(PNG.*Generated by http://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 http://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 http://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 http://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 http://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 http://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(Project)\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()); + switch(testCase.getExpectedOutVerification()) { + case EXACT: + softly.assertThat(new String(baos.toByteArray(), UTF_8)).isEqualTo(testCase.getExpectedOut()); + break; + + case REGEX: + softly.assertThat(new String(baos.toByteArray(), UTF_8)).matches(testCase.getExpectedOut()); + break; + } + } + } + + @Test + void should_readOneDiagram_return_null_for_empty_input() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream(new byte[0]), UTF_8.name()); + + String actual = pipe.readOneDiagram(); + + assertThat(actual).isNull(); + } + + @Test + void should_readOneDiagram_remove_carriage_returns() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream("@startuml\na\rb\r\nc\rd\re\n@enduml".getBytes(UTF_8)), UTF_8.name()); + + String actual = pipe.readOneDiagram(); + + assertThat(actual).isEqualTo("@startuml\nab\ncde\n@enduml"); + } + + static List startAndEndMarks() { + LinkedList linkedList = new LinkedList<>(); + linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@enduml", "@startuml\nab\ncde\n@enduml\n@enduml")); + linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@endwhatever", "@startuml\nab\ncde\n@endwhatever\n@enduml")); + linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@enduml\n", "@startuml\nab\ncde\n@enduml\n@enduml")); + linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml", "@startuml\nab\ncde\n@enduml")); + linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\n", "@startuml\nab\ncde\n@enduml")); + linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\n\n", "@startuml\nab\ncde\n@enduml")); + linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml")); + linkedList.add(InputExpected.of("this-is-garbage\n@startuml\na\rb\r\nc\rd\re\n@enduml\nthis-is-garbage\n", "@startuml\nab\ncde\n@enduml")); + linkedList.add(InputExpected.of("@startwhatever\na\rb\r\nc\rd\re\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever")); + return linkedList; + } + + @ParameterizedTest + @MethodSource("startAndEndMarks") + void should_readOneDiagram_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.readOneDiagram(); + + 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); + } + + @Test + void should_readOneLine_returns_null_if_no_input() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream(new byte[0]), UTF_8.toString()); + + String result = pipe.readOneLine(); + + assertThat(result).isNull(); + } + + @ParameterizedTest + @ValueSource(strings = {"\rab\nc\n", "\ra\rb\r\nc\n", "ab\n\rc"}) + void should_readOneLine_ignore_carriage_return_and_stop_at_line_feed(String input) throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.name()); + + String actual = pipe.readOneLine(); + + assertThat(actual).isEqualTo("ab"); + } + + @Test + void should_readOneLine_return_empty_string_if_line_feed_is_the_first_character() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream("\n".getBytes(UTF_8)), UTF_8.name()); + + String actual = pipe.readOneLine(); + + assertThat(actual).isEqualTo(""); + } + + @Test + void should_readOneLine_use_the_default_charset_if_not_provided() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream("ab\n".getBytes(Charset.defaultCharset())), null); + + String actual = pipe.readOneLine(); + + assertThat(actual).isEqualTo("ab"); + } + + @Test + void should_readOneLine_use_provided_charset() throws IOException { + pipe = new Pipe(option, null, new ByteArrayInputStream("\u2620\n".getBytes(UTF_8)), UTF_8.name()); + + String actual = pipe.readOneLine(); + + assertThat(actual).isEqualTo("\u2620"); + } + + 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 + "'"; + } + } + + 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 + "'"; + } + + } + + enum Verification { + EXACT, + REGEX + } +} From 0bef01372fd368b2abb32cb64c58d059c40a76ca Mon Sep 17 00:00:00 2001 From: Josep Mones Teixidor Date: Thu, 29 Jul 2021 06:02:06 +0200 Subject: [PATCH 3/3] Fix issue with garbage after the diagram and created better tests --- src/net/sourceforge/plantuml/Pipe.java | 226 +++++++++++--------- test/net/sourceforge/plantuml/PipeTest.java | 212 ++++++++++-------- 2 files changed, 238 insertions(+), 200 deletions(-) diff --git a/src/net/sourceforge/plantuml/Pipe.java b/src/net/sourceforge/plantuml/Pipe.java index a27ec41e6..291181d30 100644 --- a/src/net/sourceforge/plantuml/Pipe.java +++ b/src/net/sourceforge/plantuml/Pipe.java @@ -35,11 +35,15 @@ */ package net.sourceforge.plantuml; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.util.List; import net.sourceforge.plantuml.core.Diagram; @@ -51,89 +55,97 @@ import net.sourceforge.plantuml.security.SFile; public class Pipe { private final Option option; - private final InputStream is; + private final BufferedReader br; private final PrintStream ps; - private boolean closed = false; - private final String charset; private final Stdrpt stdrpt; public Pipe(Option option, PrintStream ps, InputStream is, String charset) { this.option = option; - this.is = is; + try { + this.br = new BufferedReader( + new InputStreamReader(is, (charset != null) ? charset : Charset.defaultCharset().name())); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid charset provided", e); + } this.ps = ps; - this.charset = charset; this.stdrpt = option.getStdrpt(); } public void managePipe(ErrorStatus error) throws IOException { final boolean noStdErr = option.isPipeNoStdErr(); - int nb = 0; - do { - final String source = readOneDiagram(); - if (source == null) { - ps.flush(); - if (nb == 0) { - // error.goNoData(); - } - return; - } - nb++; + + for(String source = readFirstDiagram(); source != null; source = readSubsequentDiagram()) { final Defines defines = option.getDefaultDefines(); final SFile newCurrentDir = option.getFileDir() == null ? null : new SFile(option.getFileDir()); final SourceStringReader sourceStringReader = new SourceStringReader(defines, source, "UTF-8", option.getConfig(), newCurrentDir); + if (option.isComputeurl()) { - for (BlockUml s : sourceStringReader.getBlocks()) { - ps.println(s.getEncodedUrl()); - } + computeUrlForDiagram(sourceStringReader); } else if (option.isSyntax()) { - final Diagram system = sourceStringReader.getBlocks().get(0).getDiagram(); - if (system instanceof UmlDiagram) { - error.goOk(); - ps.println(((UmlDiagram) system).getUmlDiagramType().name()); - ps.println(system.getDescription()); - } else if (system instanceof PSystemError) { - error.goWithError(); - stdrpt.printInfo(ps, (PSystemError) system); - } else { - error.goOk(); - ps.println("OTHER"); - ps.println(system.getDescription()); - } + syntaxCheckDiagram(sourceStringReader, error); } else if (option.isPipeMap()) { - final String result = sourceStringReader.getCMapData(option.getImageIndex(), - option.getFileFormatOption()); - // https://forum.plantuml.net/10049/2019-pipemap-diagrams-containing-links-give-zero-exit-code - // We don't check errors - error.goOk(); - if (result == null) { - ps.println(); - } else { - ps.println(result); - } + createPipeMapForDiagram(sourceStringReader, error); } else { - final OutputStream os = noStdErr ? new ByteArrayOutputStream() : ps; - final DiagramDescription result = sourceStringReader.outputImage(os, option.getImageIndex(), - option.getFileFormatOption()); - printInfo(noStdErr ? ps : System.err, sourceStringReader); - if (result != null && "(error)".equalsIgnoreCase(result.getDescription())) { - error.goWithError(); - } else { - error.goOk(); - if (noStdErr) { - final ByteArrayOutputStream baos = (ByteArrayOutputStream) os; - baos.close(); - ps.write(baos.toByteArray()); - } - } - if (option.getPipeDelimitor() != null) { - ps.println(option.getPipeDelimitor()); - } + generateDiagram(sourceStringReader, error, noStdErr); } ps.flush(); - } while (closed == false); - if (nb == 0) { - // error.goNoData(); + } + } + + private void generateDiagram(SourceStringReader sourceStringReader, ErrorStatus error, boolean noStdErr) throws IOException { + final OutputStream os = noStdErr ? new ByteArrayOutputStream() : ps; + final DiagramDescription result = sourceStringReader.outputImage(os, option.getImageIndex(), + option.getFileFormatOption()); + + printInfo(noStdErr ? ps : System.err, sourceStringReader); + if (result != null && "(error)".equalsIgnoreCase(result.getDescription())) { + error.goWithError(); + } else { + error.goOk(); + if (noStdErr) { + final ByteArrayOutputStream baos = (ByteArrayOutputStream) os; + baos.close(); + ps.write(baos.toByteArray()); + } + } + if (option.getPipeDelimitor() != null) { + ps.println(option.getPipeDelimitor()); + } + } + + private void createPipeMapForDiagram(SourceStringReader sourceStringReader, ErrorStatus error) throws IOException { + final String result = sourceStringReader.getCMapData(option.getImageIndex(), + option.getFileFormatOption()); + // https://forum.plantuml.net/10049/2019-pipemap-diagrams-containing-links-give-zero-exit-code + // We don't check errors + error.goOk(); + if (result == null) { + ps.println(); + } else { + ps.println(result); + } + } + + private void computeUrlForDiagram(SourceStringReader sourceStringReader) throws IOException { + for (BlockUml s : sourceStringReader.getBlocks()) { + ps.println(s.getEncodedUrl()); + } + } + + private void syntaxCheckDiagram(SourceStringReader sourceStringReader, ErrorStatus error) { + final Diagram system = sourceStringReader.getBlocks().get(0).getDiagram(); + if (system instanceof UmlDiagram) { + error.goOk(); + ps.println(((UmlDiagram) system).getUmlDiagramType().name()); + ps.println(system.getDescription()); + } else if (system instanceof PSystemError) { + error.goWithError(); + stdrpt.printInfo(ps, system); + } else { + error.goOk(); + ps.println("OTHER"); + ps.println(system.getDescription()); } } @@ -146,34 +158,55 @@ public class Pipe { } } - private boolean isFinished(String s) { - return s == null || s.startsWith("@end"); + String readFirstDiagram() throws IOException { + return readSingleDiagram(true); } - String readOneDiagram() throws IOException { + String readSubsequentDiagram() throws IOException { + return readSingleDiagram(false); + } + + String readSingleDiagram(boolean unmarkedAllowed) throws IOException { + State state = State.NO_CONTENT; + String expectedEnd = null; final StringBuilder sb = new StringBuilder(); - while (true) { - final String s = readOneLine(); - if (s == null) { - closed = true; - } else if (s.startsWith("@@@format ")) { - manageFormat(s); + + String line; + while(state != State.COMPLETE && (line = br.readLine()) != null) { + + if(line.startsWith("@@@format ")) { + manageFormat(line); } else { - sb.append(s); - sb.append(BackSlash.NEWLINE); - } - if (isFinished(s)) { - break; + if (state == State.NO_CONTENT && line.trim().length() > 0) { + state = State.START_MARK_NOT_FOUND; + } + + if (state == State.START_MARK_NOT_FOUND && line.startsWith("@start")) { + sb.setLength(0); // discard any previous input + state = State.START_MARK_FOUND; + expectedEnd = "@end" + line.substring(6).split("^[A-Za-z]")[0]; + } else if (state == State.START_MARK_FOUND && line.startsWith(expectedEnd)) { + state = State.COMPLETE; + } + + if (state != State.NO_CONTENT) { + sb.append(line).append("\n"); + } } } - String source = sb.toString().trim(); - if (sb.length() == 0) { - return null; + + switch(state) { + case NO_CONTENT: + return null; + case START_MARK_NOT_FOUND: + return (unmarkedAllowed) ? "@startuml\n" + sb.toString() + "@enduml\n" : null; + case START_MARK_FOUND: + return sb.toString() + expectedEnd; + case COMPLETE: + return sb.toString(); + default: + throw new IllegalStateException("Unexpected value: " + state); } - if (source.startsWith("@start") == false) { - source = "@startuml\n" + source + "\n@enduml"; - } - return source; } void manageFormat(String s) { @@ -184,27 +217,10 @@ public class Pipe { } } - String readOneLine() throws IOException { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - while (true) { - final int read = is.read(); - if (read == -1) { - if (baos.size() == 0) { - return null; - } - break; - } - if (read != '\r' && read != '\n') { - baos.write(read); - } - if (read == '\n') { - break; - } - } - if (charset == null) { - return new String(baos.toByteArray()); - } - return new String(baos.toByteArray(), charset); - + enum State { + NO_CONTENT, + START_MARK_NOT_FOUND, + START_MARK_FOUND, + COMPLETE } } diff --git a/test/net/sourceforge/plantuml/PipeTest.java b/test/net/sourceforge/plantuml/PipeTest.java index 57c4ea01a..35f1c1cd3 100644 --- a/test/net/sourceforge/plantuml/PipeTest.java +++ b/test/net/sourceforge/plantuml/PipeTest.java @@ -3,6 +3,7 @@ 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; @@ -154,43 +155,109 @@ class PipeTest { } @Test - void should_readOneDiagram_return_null_for_empty_input() throws IOException { + 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.readOneDiagram(); + String actual = pipe.readFirstDiagram(); assertThat(actual).isNull(); } @Test - void should_readOneDiagram_remove_carriage_returns() throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream("@startuml\na\rb\r\nc\rd\re\n@enduml".getBytes(UTF_8)), UTF_8.name()); + 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.readOneDiagram(); + String actual = pipe.readFirstDiagram(); - assertThat(actual).isEqualTo("@startuml\nab\ncde\n@enduml"); + assertThat(actual).isEqualTo("@startuml\n\u2620\n@enduml\n"); } - static List startAndEndMarks() { - LinkedList linkedList = new LinkedList<>(); - linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@enduml", "@startuml\nab\ncde\n@enduml\n@enduml")); - linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@endwhatever", "@startuml\nab\ncde\n@endwhatever\n@enduml")); - linkedList.add(InputExpected.of("\na\rb\r\nc\rd\re\n@enduml\n", "@startuml\nab\ncde\n@enduml\n@enduml")); - linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml", "@startuml\nab\ncde\n@enduml")); - linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\n", "@startuml\nab\ncde\n@enduml")); - linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\n\n", "@startuml\nab\ncde\n@enduml")); - linkedList.add(InputExpected.of("@startuml\na\rb\r\nc\rd\re\n@enduml\r\n\r\n", "@startuml\nab\ncde\n@enduml")); - linkedList.add(InputExpected.of("this-is-garbage\n@startuml\na\rb\r\nc\rd\re\n@enduml\nthis-is-garbage\n", "@startuml\nab\ncde\n@enduml")); - linkedList.add(InputExpected.of("@startwhatever\na\rb\r\nc\rd\re\n@endwhatever", "@startwhatever\nab\ncde\n@endwhatever")); - return linkedList; + // 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 - @MethodSource("startAndEndMarks") - void should_readOneDiagram_handle_correctly_start_and_end_marks(InputExpected inputExpected) throws IOException { + @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")); + 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.readOneDiagram(); + 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")); + 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()); } @@ -219,81 +286,8 @@ class PipeTest { assertThat(option.getFileFormatOption().getFileFormat()).isEqualTo(FileFormat.PNG); } - @Test - void should_readOneLine_returns_null_if_no_input() throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream(new byte[0]), UTF_8.toString()); - - String result = pipe.readOneLine(); - - assertThat(result).isNull(); - } - - @ParameterizedTest - @ValueSource(strings = {"\rab\nc\n", "\ra\rb\r\nc\n", "ab\n\rc"}) - void should_readOneLine_ignore_carriage_return_and_stop_at_line_feed(String input) throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.name()); - - String actual = pipe.readOneLine(); - - assertThat(actual).isEqualTo("ab"); - } - - @Test - void should_readOneLine_return_empty_string_if_line_feed_is_the_first_character() throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream("\n".getBytes(UTF_8)), UTF_8.name()); - - String actual = pipe.readOneLine(); - - assertThat(actual).isEqualTo(""); - } - - @Test - void should_readOneLine_use_the_default_charset_if_not_provided() throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream("ab\n".getBytes(Charset.defaultCharset())), null); - - String actual = pipe.readOneLine(); - - assertThat(actual).isEqualTo("ab"); - } - - @Test - void should_readOneLine_use_provided_charset() throws IOException { - pipe = new Pipe(option, null, new ByteArrayInputStream("\u2620\n".getBytes(UTF_8)), UTF_8.name()); - - String actual = pipe.readOneLine(); - - assertThat(actual).isEqualTo("\u2620"); - } - - 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 + "'"; - } - } - static class TestCase { + private final String options; private final String input; private final String expectedOut; @@ -346,6 +340,34 @@ class PipeTest { } + 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