Skip to content

Added import error detection to RunSpaAndExecuteMochaTestsExecutionStrategySeparateTests. #1632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Docker/worker_base/js/setup_node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ npm install --prefix /judge-resources/js/v20
npm install --prefix /judge-resources/js/v20/js-run-spa-in-docker-and-execute-mocha-tests
npm install -g typescript

# Install ESLint and its plugins globally
npm install -g eslint eslint-plugin-import

# Update /usr/bin/node to point to the currently active Node.js version:
ln -sf "$(which node)" /usr/bin/node
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"NodeJsExecutablePath": "/home/doncho/.nvm/versions/node/v12.13.0/bin/node",
"NodeJs20ExecutablePath": "/root/.nvm/versions/node/v20.12.0/bin/node",
"UnderscoreModulePath": "{nodeResourcesPath}/node_modules/underscore",
"EslintExecutablePath": "/root/.nvm/versions/node/v20.12.0/bin/eslint",
"EslintPluginModulePath": "/root/.nvm/versions/node/v20.12.0/lib/node_modules/eslint-plugin-import",
"MochaModulePath": "{nodeResourcesPath}/node_modules/mocha/bin/_mocha",
"ChaiModulePath": "{nodeResourcesPath}/node_modules/chai",
"JsDomModulePath": "{nodeResourcesPath}/node_modules/jsdom",
Expand Down
2 changes: 2 additions & 0 deletions Servers/Worker/OJS.Servers.Worker/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"NodeJsExecutablePath": "/root/.nvm/versions/node/v12.22.12/bin/node",
"NodeJs20ExecutablePath": "/root/.nvm/versions/node/v20.12.0/bin/node",
"TypeScriptExecutablePath": "/root/.nvm/versions/node/v20.12.0/bin/tsc",
"EslintExecutablePath": "/root/.nvm/versions/node/v20.12.0/bin/eslint",
"EslintPluginModulePath": "/root/.nvm/versions/node/v20.12.0/lib/node_modules/eslint-plugin-import",
"PythonExecutablePath": "/usr/bin/python3.10",
"PhpCliExecutablePath": "/usr/bin/php",
"PhpCgiExecutablePath": "/usr/bin/php-cgi",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace OJS.Workers.ExecutionStrategies.Eslint;

using System.Text.Json.Serialization;

public class EslintError
{
[JsonPropertyName("filePath")]
public string FilePath { get; set; } = string.Empty;

[JsonPropertyName("messages")]
public List<EslintMessage> Messages { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace OJS.Workers.ExecutionStrategies.Eslint;

using System.Text.Json.Serialization;

public class EslintMessage
{
[JsonPropertyName("line")]
public int Line { get; set; }

[JsonPropertyName("column")]
public int Column { get; set; }

[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
Expand Up @@ -559,5 +559,7 @@ public record RunSpaAndExecuteMochaTestsExecutionStrategySettings(
string JsProjNodeModulesPath,
string MochaModulePath,
string ChaiModulePath,
string PlaywrightChromiumModulePath)
string PlaywrightChromiumModulePath,
string EslintExecutablePath,
string EslintPluginModulePath)
: PythonExecuteAndCheckExecutionStrategySettings(BaseTimeUsed, BaseMemoryUsed, PythonExecutablePath);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace OJS.Workers.ExecutionStrategies;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using Ionic.Zip;
using Microsoft.Extensions.Logging;
Expand All @@ -12,6 +13,7 @@ namespace OJS.Workers.ExecutionStrategies;
using OJS.Workers.Common.Extensions;
using OJS.Workers.Common.Helpers;
using OJS.Workers.Common.Models;
using OJS.Workers.ExecutionStrategies.Eslint;
using OJS.Workers.ExecutionStrategies.Models;
using OJS.Workers.ExecutionStrategies.Python;
using OJS.Workers.Executors;
Expand All @@ -30,7 +32,8 @@ public class RunSpaAndExecuteMochaTestsExecutionStrategySeparateTests<TSettings>
private const string TestsDirectoryName = "test";
private const string UserApplicationDirectoryName = "app";
private const string NginxConfFileName = "nginx.conf";
private readonly Regex testTimeoutRegex = new Regex(@"Timeout (?:of )?\d+ms exceeded\.");
private readonly Regex testTimeoutRegex = new(@"Timeout (?:of )?\d+ms exceeded\.");
private const string EslintConfigFileName = "eslint.config.js";

public RunSpaAndExecuteMochaTestsExecutionStrategySeparateTests(
IOjsSubmission submission,
Expand All @@ -39,7 +42,47 @@ public RunSpaAndExecuteMochaTestsExecutionStrategySeparateTests(
ILogger<BaseExecutionStrategy<TSettings>> logger)
: base(submission, processExecutorFactory, settingsProvider, logger)
{
if (!FileHelpers.FileExists(this.Settings.EslintExecutablePath))
{
throw new ArgumentException($"Eslint not found in: {this.Settings.EslintExecutablePath}", nameof(this.Settings.EslintExecutablePath));
}
}

private string EslintConfigContent => @"
const importPlugin = require('" + this.Settings.EslintPluginModulePath + @"');

module.exports = [
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2021,
sourceType: 'module',
globals: {
window: 'readonly',
document: 'readonly',
console: 'readonly',
},
},
plugins: {
import: importPlugin,
},
rules: {
'import/no-unresolved': ['error', { caseSensitive: true }],
'import/named': 'error',
'import/default': 'error',
'import/namespace': 'error',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js'],
}
}
}
}
];";


private static string NginxFileContent => $@"
worker_processes 1;

Expand Down Expand Up @@ -247,6 +290,7 @@ protected override async Task<IExecutionResult<TestResult>> ExecuteAgainstTestsI
}

this.SaveNginxFile();
this.SaveEslintConfig();

var preExecuteCodeSavePath = this.SavePythonCodeTemplateToTempFile(this.PythonPreExecuteCodeTemplate);
var executor = this.CreateStandardExecutor();
Expand All @@ -267,6 +311,15 @@ protected override async Task<IExecutionResult<TestResult>> ExecuteAgainstTestsI
throw new ArgumentException("Failed to run the strategy pre execute step. Please contact a developer.");
}

// Run ESLint validation before running tests
var eslintResult = await this.RunEslintValidation(executor, executionContext);
if (!string.IsNullOrEmpty(eslintResult))
{
result.IsCompiledSuccessfully = false;
result.CompilerComment = $"ESLint validation failed:\n{eslintResult}";
return result;
}

return await this.RunTests(string.Empty, executor, checker, executionContext, result);
}
catch (Exception)
Expand Down Expand Up @@ -569,4 +622,48 @@ import docker

return await this.Execute(executionContext, executor, cleanupFilePath);
}

private void SaveEslintConfig()
{
var eslintConfigPath = FileHelpers.BuildPath(this.UserApplicationPath, EslintConfigFileName);
FileHelpers.SaveStringToFile(this.EslintConfigContent, eslintConfigPath);
}

private async Task<string> RunEslintValidation(IExecutor executor, IExecutionContext<TestsInputModel> executionContext)
{
var arguments = new[]
{
"--format", "json",
};

var processExecutionResult = await executor.Execute(
this.Settings.EslintExecutablePath,
executionContext.TimeLimit,
executionContext.MemoryLimit,
executionArguments: arguments,
workingDirectory: this.UserApplicationPath);

if (processExecutionResult.ExitCode != 0)
{
try
{
var errors = JsonSerializer.Deserialize<List<EslintError>>(processExecutionResult.ReceivedOutput);
if (errors != null)
{
var formattedErrors = errors
.SelectMany(error => error.Messages
.Where(message => !message.Message.Contains("node_modules"))
.Select(message => $"{error.FilePath}:{message.Line}:{message.Column} - {message.Message}"));

return string.Join(Environment.NewLine, formattedErrors);
}
}
catch (JsonException)
{
return processExecutionResult.ReceivedOutput;
}
}

return string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,9 @@ ExecutionStrategyType.NodeJsV20RunSpaAndExecuteMochaTestsExecutionStrategySepara
this.GetNodeResourcePath(executionStrategyType, this.settings.JsProjNodeModules),
this.GetNodeResourcePath(executionStrategyType, this.settings.MochaModulePath),
this.GetNodeResourcePath(executionStrategyType, this.settings.ChaiModulePath),
this.GetNodeResourcePath(executionStrategyType, this.settings.PlaywrightChromiumModulePath))
this.GetNodeResourcePath(executionStrategyType, this.settings.PlaywrightChromiumModulePath),
this.settings.EslintExecutablePath,
this.settings.EslintPluginModulePath)

as TSettings,
ExecutionStrategyType.GolangCompileExecuteAndCheck => new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ public class OjsWorkersConfig : BaseConfig
[Required]
public string PythonExecutablePath { get; set; } = string.Empty;

[Required]
public string EslintExecutablePath { get; set; } = string.Empty;

[Required]
public string EslintPluginModulePath { get; set; } = string.Empty;

[Required]
public string SqlServerLocalDbMasterDbConnectionString { get; set; } = string.Empty;

Expand Down