GoLanguageTest.java 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package de.jplag.golang;
  2. import static org.junit.jupiter.api.Assertions.assertTrue;
  3. import static org.junit.jupiter.api.Assertions.fail;
  4. import java.io.File;
  5. import java.io.IOException;
  6. import java.nio.file.Files;
  7. import java.nio.file.Path;
  8. import java.util.ArrayList;
  9. import java.util.Arrays;
  10. import java.util.List;
  11. import java.util.Objects;
  12. import java.util.OptionalInt;
  13. import java.util.Set;
  14. import java.util.stream.Collectors;
  15. import java.util.stream.IntStream;
  16. import org.junit.jupiter.api.BeforeEach;
  17. import org.junit.jupiter.api.Test;
  18. import org.slf4j.Logger;
  19. import org.slf4j.LoggerFactory;
  20. import de.jplag.SharedTokenType;
  21. import de.jplag.Token;
  22. import de.jplag.TokenPrinter;
  23. class GoLanguageTest {
  24. /**
  25. * Test source file that is supposed to produce a complete set of tokens, i.e. all types of tokens.
  26. */
  27. private static final String COMPLETE_TEST_FILE = "Complete.go";
  28. /**
  29. * Regular expression that describes lines consisting only of whitespace and optionally a line comment.
  30. */
  31. private static final String EMPTY_OR_SINGLE_LINE_COMMENT = "\\s*(//.*|/\\*.*\\*/)?";
  32. /**
  33. * Regular expression that describes lines containing the start of a multiline comment and no code before it.
  34. */
  35. private static final String DELIMITED_COMMENT_START = "\\s*/\\*(?:(?!\\*/).)*$";
  36. /**
  37. * Regular expression that describes lines containing the end of a multiline comment and no more code after that.
  38. */
  39. private static final String DELIMITED_COMMENT_END = ".*\\*/\\s*$";
  40. private final Logger logger = LoggerFactory.getLogger("GoLang language test");
  41. private final String[] testFiles = new String[] {COMPLETE_TEST_FILE};
  42. private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "golang").toFile();
  43. private Language language;
  44. @BeforeEach
  45. void setup() {
  46. language = new Language();
  47. }
  48. @Test
  49. void parseTestFiles() {
  50. for (String fileName : testFiles) {
  51. List<Token> tokens = language.parse(Set.of(new File(testFileLocation, fileName)));
  52. String output = TokenPrinter.printTokens(tokens, testFileLocation);
  53. logger.info(output);
  54. testSourceCoverage(fileName, tokens);
  55. if (fileName.equals(COMPLETE_TEST_FILE)) {
  56. testTokenCoverage(tokens, fileName);
  57. }
  58. }
  59. }
  60. /**
  61. * Confirms that the code is covered to a basic extent, i.e. each line of code contains at least one token.
  62. * @param fileName a code sample file name
  63. * @param tokens the list of tokens generated from the sample
  64. */
  65. private void testSourceCoverage(String fileName, List<Token> tokens) {
  66. File testFile = new File(testFileLocation, fileName);
  67. List<String> lines = null;
  68. try {
  69. lines = Files.readAllLines(testFile.toPath());
  70. } catch (IOException exception) {
  71. logger.info("Error while reading test file %s".formatted(fileName), exception);
  72. fail();
  73. }
  74. // All lines that contain code
  75. var codeLines = getCodeLines(lines);
  76. // All lines that contain a token
  77. var tokenLines = tokens.stream().mapToInt(Token::getLine).distinct().boxed().toList();
  78. if (codeLines.size() > tokenLines.size()) {
  79. List<Integer> missedLinesIndices = new ArrayList<>(codeLines);
  80. missedLinesIndices.removeAll(tokenLines);
  81. var missedLines = missedLinesIndices.stream().map(Object::toString).collect(Collectors.joining(", "));
  82. if (!missedLines.isBlank()) {
  83. fail("Found lines in file '%s' that are not represented in the token list. \n\tMissed lines: %s".formatted(fileName, missedLines));
  84. }
  85. }
  86. OptionalInt differingLine = IntStream.range(0, codeLines.size())
  87. .dropWhile(index -> Objects.equals(codeLines.get(index), tokenLines.get(index))).findAny();
  88. differingLine.ifPresent(
  89. i -> fail("Not all lines of code in '%s' are represented in tokens, starting with line %d.".formatted(fileName, codeLines.get(i))));
  90. }
  91. /**
  92. * Gets the line numbers of lines containing actual code, omitting empty lines and comment lines.
  93. * @param lines lines of a code file
  94. * @return an array of the line numbers of code lines
  95. */
  96. private List<Integer> getCodeLines(List<String> lines) {
  97. // This boxed boolean can be accessed from within the lambda method below
  98. var state = new Object() {
  99. boolean insideComment = false;
  100. };
  101. var codeLines = IntStream.rangeClosed(1, lines.size()).sequential().filter(idx -> {
  102. String line = lines.get(idx - 1);
  103. if (line.matches(EMPTY_OR_SINGLE_LINE_COMMENT)) {
  104. return false;
  105. } else if (line.matches(DELIMITED_COMMENT_START)) {
  106. state.insideComment = true;
  107. return false;
  108. } else if (state.insideComment) {
  109. // This fails if code follows after '*/'. If the code is formatted well, this should not happen.
  110. if (line.matches(DELIMITED_COMMENT_END)) {
  111. state.insideComment = false;
  112. }
  113. return false;
  114. }
  115. return true;
  116. });
  117. return codeLines.boxed().toList();
  118. }
  119. /**
  120. * Confirms that all Token types are 'reachable' with a complete code example.
  121. * @param tokens list of tokens which is supposed to contain all types of tokens
  122. * @param fileName The file name of the complete code example
  123. */
  124. private void testTokenCoverage(List<Token> tokens, String fileName) {
  125. var annotatedTokens = tokens.stream().map(Token::getType).collect(Collectors.toSet());
  126. assertTrue(annotatedTokens.contains(SharedTokenType.FILE_END));
  127. var annotatedGoTokens = annotatedTokens.stream().filter(GoTokenType.class::isInstance).collect(Collectors.toSet());
  128. var allGoTokens = GoTokenType.values();
  129. var missingGoTokens = Arrays.stream(allGoTokens).filter(token -> !annotatedGoTokens.contains(token)).toList();
  130. assertTrue(missingGoTokens.isEmpty(), "The following go tokens are missing in the code example '%s':\n".formatted(fileName)
  131. + String.join("\n", missingGoTokens.stream().map(GoTokenType::getDescription).toList()));
  132. }
  133. }