RustFrontendTest.java 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. package de.jplag.rust;
  2. import static org.junit.jupiter.api.Assertions.assertTrue;
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.nio.file.Files;
  6. import java.nio.file.Path;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. import java.util.stream.IntStream;
  10. import org.junit.jupiter.api.BeforeEach;
  11. import org.junit.jupiter.api.Test;
  12. import org.slf4j.Logger;
  13. import org.slf4j.LoggerFactory;
  14. import de.jplag.Token;
  15. import de.jplag.TokenConstants;
  16. import de.jplag.TokenList;
  17. import de.jplag.TokenPrinter;
  18. class RustFrontendTest {
  19. /**
  20. * Regular expression for empty lines and single line comments.
  21. */
  22. private static final String RUST_EMPTY_OR_SINGLE_LINE_COMMENT = "\\s*(?://.*)?";
  23. private static final String RUST_MULTILINE_COMMENT_BEGIN = "\\s*/\\*.*";
  24. private static final String RUST_MULTILINE_COMMENT_END = ".*\\*/\\s*";
  25. /**
  26. * Test source file that is supposed to produce a complete set of tokens, i.e. all types of tokens.
  27. */
  28. private static final String COMPLETE_TEST_FILE = "complete.rs";
  29. public static final int NOT_SET = -1;
  30. private static final String EMPTY_STRING = "";
  31. private static final String RUST_SHEBANG = "#!.*$";
  32. private static final double EPSILON = 1E-6;
  33. public static final double BASELINE_COVERAGE = 0.75;
  34. private final Logger logger = LoggerFactory.getLogger("Rust frontend test");
  35. private final String[] testFiles = new String[] {"deno_core_runtime.rs", COMPLETE_TEST_FILE};
  36. private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "rust").toFile();
  37. private Language language;
  38. @BeforeEach
  39. void setup() {
  40. language = new Language();
  41. }
  42. @Test
  43. void parseTestFiles() {
  44. for (String fileName : testFiles) {
  45. TokenList tokens = language.parse(testFileLocation, new String[] {fileName});
  46. String output = TokenPrinter.printTokens(tokens, testFileLocation);
  47. logger.info(output);
  48. testSourceCoverage(fileName, tokens);
  49. if (fileName.equals(COMPLETE_TEST_FILE))
  50. testTokenCoverage(tokens, fileName);
  51. }
  52. }
  53. /**
  54. * Confirms that the code is covered to a basic extent, i.e. each line of code contains at least one token.
  55. * @param fileName a code sample file name
  56. * @param tokens the TokenList generated from the sample
  57. */
  58. private void testSourceCoverage(String fileName, TokenList tokens) {
  59. File testFile = new File(testFileLocation, fileName);
  60. try {
  61. List<String> lines = Files.readAllLines(testFile.toPath());
  62. // All lines that contain code
  63. var codeLines = new ArrayList<>(getCodeLines(lines));
  64. // All lines that contain token
  65. var tokenLines = IntStream.range(0, tokens.size()).mapToObj(tokens::getToken).mapToInt(Token::getLine).distinct().boxed().toList();
  66. // Keep only lines that have no tokens
  67. codeLines.removeAll(tokenLines);
  68. double coverage = 1.d - (codeLines.size() * 1.d / (codeLines.size() + tokenLines.size()));
  69. if (coverage == 1) {
  70. logger.info("All lines covered.");
  71. } else {
  72. logger.info("Coverage: %.1f%%.".formatted(coverage * 100));
  73. logger.info("Missing lines {}", codeLines);
  74. assertTrue(coverage - BASELINE_COVERAGE >= EPSILON, "Source coverage is unsatisfactory");
  75. }
  76. } catch (IOException exception) {
  77. logger.info("Error while reading test file %s".formatted(fileName), exception);
  78. assertTrue(false);
  79. }
  80. }
  81. private List<Integer> getCodeLines(List<String> lines) {
  82. var state = new Object() {
  83. boolean insideMultilineComment = false;
  84. };
  85. return IntStream.range(1, lines.size() + 1).sequential().filter(idx -> {
  86. String line = lines.get(idx - 1);
  87. if (line.matches(RUST_EMPTY_OR_SINGLE_LINE_COMMENT)) {
  88. return false;
  89. } else if (idx == 1 && line.matches(RUST_SHEBANG)) {
  90. return false;
  91. } else if (line.matches(RUST_MULTILINE_COMMENT_BEGIN)) {
  92. state.insideMultilineComment = true;
  93. return false;
  94. } else if (state.insideMultilineComment && line.matches(RUST_MULTILINE_COMMENT_END)) {
  95. state.insideMultilineComment = false;
  96. return false;
  97. } else {
  98. return !state.insideMultilineComment;
  99. }
  100. }).boxed().toList();
  101. }
  102. /**
  103. * Confirms that all Token types are 'reachable' with a complete code example.
  104. * @param tokens TokenList which is supposed to contain all types of tokens
  105. * @param fileName The file name of the complete code example
  106. */
  107. private void testTokenCoverage(TokenList tokens, String fileName) {
  108. var foundTokens = tokens.allTokens().stream().mapToInt(Token::getType).distinct().boxed().toList();
  109. var allTokens = IntStream.range(0, RustTokenConstants.NUMBER_DIFF_TOKENS).boxed().toList();
  110. allTokens = new ArrayList<>(allTokens);
  111. // Only non-found tokens are left
  112. allTokens.removeAll(foundTokens);
  113. // Exclude SEPARATOR_TOKEN, as it does not occur
  114. allTokens.remove((Integer) (TokenConstants.SEPARATOR_TOKEN));
  115. if (!allTokens.isEmpty()) {
  116. var notFoundTypes = allTokens.stream().map(type -> new RustToken(type, EMPTY_STRING, NOT_SET, NOT_SET, NOT_SET).type2string()).toList();
  117. logger.error("Some %d token types were not found in the complete code example '%s':\n%s".formatted(notFoundTypes.size(), fileName,
  118. notFoundTypes));
  119. assertTrue(false);
  120. }
  121. }
  122. }