diff --git a/src/main/java/org/hyperskill/hstest/checker/CheckLibraryVersion.java b/src/main/java/org/hyperskill/hstest/checker/CheckLibraryVersion.java new file mode 100644 index 00000000..9d74b00a --- /dev/null +++ b/src/main/java/org/hyperskill/hstest/checker/CheckLibraryVersion.java @@ -0,0 +1,108 @@ +package org.hyperskill.hstest.checker; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.time.LocalDate; +import java.util.Map; +import java.util.stream.Collectors; + +/** +* Checks if the current version of the library is the latest one on GitHub releases page of the library. +* If not, throws an exception. + */ +public class CheckLibraryVersion { + + private String VERSION_FILE = "src/main/java/org/hyperskill/hstest/resources/version.txt"; + private String LAST_CHECKED_FILE = "lastCheckedHSTestLibrary.txt"; + private String GITHUB_API = "https://api.github.com/repos/hyperskill/hs-test/releases/latest"; + private String currentVersion; + private String latestVersion; + public boolean isLatestVersion = true; + + public CheckLibraryVersion() { + } + + /** + * Checks if the current version of the library is the latest one on GitHub releases page of the library. + * If not, throws an exception. + */ + public void checkVersion() throws IOException { + LocalDate lastChecked = null; + String tempDirectoryPath = System.getProperty("java.io.tmpdir"); + File lastCheckedFile = new File(tempDirectoryPath + File.separator + LAST_CHECKED_FILE); + if (lastCheckedFile.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(lastCheckedFile))) { + lastChecked = LocalDate.parse(reader.readLine()); + } + } + if (LocalDate.now().equals(lastChecked)) { + return; + } + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(VERSION_FILE); + if (inputStream != null) { + currentVersion = new BufferedReader(new InputStreamReader(inputStream)).readLine(); + } else return; + latestVersion = getLatestHsTestVersionFromGitHub(); + if (!currentVersion.equals(latestVersion)) { + isLatestVersion = false; + } + lastChecked = LocalDate.now(); + try (FileWriter writer = new FileWriter(lastCheckedFile)) { + writer.write(lastChecked.toString()); + } + } + + /** + * Returns latest version of the library from GitHub releases page of the library. + * @return String latest version of the library + */ + private String getLatestHsTestVersionFromGitHub() { + HttpURLConnection connection = null; + int responseCode = -1; + try { + URL url = new URL(GITHUB_API); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/vnd.github+json"); + connection.setConnectTimeout(100); + connection.setReadTimeout(500); + responseCode = connection.getResponseCode(); + } catch (IOException e) { + return currentVersion; + } + if (responseCode != 200) { + return currentVersion; + } + + try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String response = in.lines().collect(Collectors.joining()); + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + Map map = gson.fromJson(response, type); + return map.get("tag_name").toString().replace("v", ""); + } catch (IOException e) { + return currentVersion; + } + } + + /** + * Returns feedback for the user if the current version of the library is not the latest one. + * @return String feedback for the user + */ + public String getFeedback() { + return "\nThe installed hs-test version (" + currentVersion + ") is not the latest version (" + latestVersion + "). " + + "Update the library by following the instructions below:\n\n" + + "1. Open your project's dependency file build.gradle.\n" + + "2. Find the hs-test dependency and change its version to the latest one (" + latestVersion + ").\n" + + "3. Sync the dependencies in your development environment or run the following commands in the terminal:\n" + + " For Gradle:\n" + + " gradle clean build --refresh-dependencies\n\n" + + "4. Restart the tests.\n\n"; + } +} diff --git a/src/main/java/org/hyperskill/hstest/resources/version.txt b/src/main/java/org/hyperskill/hstest/resources/version.txt new file mode 100644 index 00000000..6eb2d9f3 --- /dev/null +++ b/src/main/java/org/hyperskill/hstest/resources/version.txt @@ -0,0 +1 @@ +10.0.3 \ No newline at end of file diff --git a/src/main/java/org/hyperskill/hstest/stage/StageTest.java b/src/main/java/org/hyperskill/hstest/stage/StageTest.java index a64309dd..c3982826 100644 --- a/src/main/java/org/hyperskill/hstest/stage/StageTest.java +++ b/src/main/java/org/hyperskill/hstest/stage/StageTest.java @@ -1,6 +1,7 @@ package org.hyperskill.hstest.stage; import lombok.Getter; +import org.hyperskill.hstest.checker.CheckLibraryVersion; import org.hyperskill.hstest.common.FileUtils; import org.hyperskill.hstest.common.ReflectionUtils; import org.hyperskill.hstest.dynamic.ClassSearcher; @@ -14,10 +15,7 @@ import org.hyperskill.hstest.testcase.TestCase; import org.hyperskill.hstest.testing.TestRun; import org.hyperskill.hstest.testing.execution.MainMethodExecutor; -import org.hyperskill.hstest.testing.execution.process.GoExecutor; -import org.hyperskill.hstest.testing.execution.process.JavascriptExecutor; -import org.hyperskill.hstest.testing.execution.process.PythonExecutor; -import org.hyperskill.hstest.testing.execution.process.ShellExecutor; +import org.hyperskill.hstest.testing.execution.process.*; import org.hyperskill.hstest.testing.runner.AsyncDynamicTestingRunner; import org.hyperskill.hstest.testing.runner.TestRunner; import org.junit.Test; @@ -84,6 +82,9 @@ private TestRunner initRunner() { for (var folder : walkUserFiles(FileUtils.cwd())) { for (var file : folder.getFiles()) { + if (file.getName().endsWith(".cpp")) { + return new AsyncDynamicTestingRunner(CppExecutor.class); + } if (file.getName().endsWith(".go")) { return new AsyncDynamicTestingRunner(GoExecutor.class); } @@ -162,10 +163,14 @@ public final void start() { currTestRun = testRun; CheckResult result = testRun.test(); - if (!result.isCorrect()) { + CheckLibraryVersion checkLibraryVersion = new CheckLibraryVersion(); + checkLibraryVersion.checkVersion(); String fullFeedback = result.getFeedback() + "\n\n" + testRun.getTestCase().getFeedback(); + if (!checkLibraryVersion.isLatestVersion) { + fullFeedback += checkLibraryVersion.getFeedback(); + } throw new WrongAnswer(fullFeedback.trim()); } diff --git a/src/main/java/org/hyperskill/hstest/testing/execution/process/CppExecutor.java b/src/main/java/org/hyperskill/hstest/testing/execution/process/CppExecutor.java new file mode 100644 index 00000000..af7c245b --- /dev/null +++ b/src/main/java/org/hyperskill/hstest/testing/execution/process/CppExecutor.java @@ -0,0 +1,70 @@ +package org.hyperskill.hstest.testing.execution.process; + +import org.hyperskill.hstest.testing.execution.ProcessExecutor; +import org.hyperskill.hstest.testing.execution.searcher.CppSearcher; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hyperskill.hstest.common.FileUtils.abspath; +import static org.hyperskill.hstest.common.OsUtils.isWindows; + +/** + * Executes C++ runnable files + * (files with main function) + * in the given directory. + * + */ +public class CppExecutor extends ProcessExecutor { + private final String executable; + private final String filename; + + public CppExecutor(String sourceName) { + super(new CppSearcher().find(sourceName)); + + var fileName = runnable.getFile().getName(); + + var withoutCpp = fileName + .substring(0, fileName.length() - new CppSearcher().extension().length()); + + if (isWindows()) { + executable = withoutCpp; + filename = executable + ".exe"; + } else { + executable = "./" + withoutCpp; + filename = withoutCpp; + } + } + + @Override + protected List compilationCommand() { + return List.of("g++", "-std", "c++20", "-pipe", "-O2", "-static", "-o", filename, runnable.getFile().getName()); + } + + @Override + protected String filterCompilationError(String error) { + // Adapt error filtering if needed + return error; + } + + @Override + protected List executionCommand(List args) { + List fullArgs = new ArrayList<>(); + fullArgs.add(executable); + fullArgs.addAll(args); + + return fullArgs; + } + + @Override + protected void cleanup() { + try { + Files.deleteIfExists(Paths.get(abspath(filename))); + } catch (IOException ignored) { } + } +} + diff --git a/src/main/java/org/hyperskill/hstest/testing/execution/searcher/CppSearcher.java b/src/main/java/org/hyperskill/hstest/testing/execution/searcher/CppSearcher.java new file mode 100644 index 00000000..14cb144b --- /dev/null +++ b/src/main/java/org/hyperskill/hstest/testing/execution/searcher/CppSearcher.java @@ -0,0 +1,24 @@ +package org.hyperskill.hstest.testing.execution.searcher; + +import org.hyperskill.hstest.testing.execution.runnable.RunnableFile; + +/** + * Searches for C++ runnable files + * (files with main function) + * in the given directory + * and returns the first one found. + */ +public class CppSearcher extends BaseSearcher { + @Override + public String extension() { + return ".cpp"; + } + + @Override + public RunnableFile search(String whereToSearch) { + return simpleSearch(whereToSearch, + "int main()", + "(^|\\n)\\s*int\\s+main\\s*\\(.*\\)" + ); + } +}