mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
Compare commits
10 Commits
salmanmkc/
...
salmanmkc/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
820bcaf258 | ||
|
|
1f9418d6c0 | ||
|
|
49b30c8a23 | ||
|
|
577c73ee80 | ||
|
|
2dc3c3adc1 | ||
|
|
ffc2086972 | ||
|
|
63ada10762 | ||
|
|
c824407a9b | ||
|
|
f06a45283d | ||
|
|
3aef228adc |
@@ -218,4 +218,4 @@ if [ $restartinteractiverunner -ne 0 ]
|
|||||||
then
|
then
|
||||||
date "+[%F %T-%4N] Restarting interactive runner" >> "$logfile.succeed" 2>&1
|
date "+[%F %T-%4N] Restarting interactive runner" >> "$logfile.succeed" 2>&1
|
||||||
"$rootfolder/run.sh" &
|
"$rootfolder/run.sh" &
|
||||||
fi
|
fi
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Linq;
|
||||||
using GitHub.Runner.Common.Tests;
|
using GitHub.Runner.Common.Tests;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -10,8 +11,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
{
|
{
|
||||||
public sealed class ShellScriptSyntaxL0
|
public sealed class ShellScriptSyntaxL0
|
||||||
{
|
{
|
||||||
// Generic method to test any shell script template for bash syntax errors
|
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null, bool useFullPath = false, bool useShellCheck = true)
|
||||||
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null)
|
|
||||||
{
|
{
|
||||||
// Skip on Windows
|
// Skip on Windows
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
@@ -24,64 +24,103 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
string templatePath;
|
||||||
string templatePath = Path.Combine(rootDirectory, relativePath, templateName);
|
|
||||||
|
if (useFullPath)
|
||||||
|
{
|
||||||
|
templatePath = templateName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||||
|
templatePath = Path.Combine(rootDirectory, relativePath, templateName);
|
||||||
|
}
|
||||||
|
|
||||||
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
Directory.CreateDirectory(tempDir);
|
Directory.CreateDirectory(tempDir);
|
||||||
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
|
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
|
||||||
|
string debugLogPath = Path.Combine(tempDir, "debug_log.txt");
|
||||||
|
|
||||||
// Read the template
|
|
||||||
string template = File.ReadAllText(templatePath);
|
string template = File.ReadAllText(templatePath);
|
||||||
|
|
||||||
// Apply template modifier if provided (for injecting errors)
|
|
||||||
if (templateModifier != null)
|
if (templateModifier != null)
|
||||||
{
|
{
|
||||||
template = templateModifier(template);
|
template = templateModifier(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace common placeholders with valid test values
|
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||||
template = ReplaceCommonPlaceholders(template, rootDirectory, tempDir);
|
template = ReplaceCommonPlaceholders(template, rootFolder, tempDir);
|
||||||
|
|
||||||
// Write the processed template to a temporary file
|
|
||||||
File.WriteAllText(tempScriptPath, template);
|
File.WriteAllText(tempScriptPath, template);
|
||||||
|
|
||||||
// Make the file executable
|
|
||||||
var chmodProcess = new Process();
|
var chmodProcess = new Process();
|
||||||
chmodProcess.StartInfo.FileName = "chmod";
|
chmodProcess.StartInfo.FileName = "chmod";
|
||||||
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
|
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
|
||||||
chmodProcess.Start();
|
chmodProcess.Start();
|
||||||
chmodProcess.WaitForExit();
|
chmodProcess.WaitForExit();
|
||||||
|
|
||||||
|
var bashCheckProcess = new Process();
|
||||||
|
bashCheckProcess.StartInfo.FileName = "/bin/bash";
|
||||||
|
bashCheckProcess.StartInfo.Arguments = $"-c \"bash -n {tempScriptPath}; echo $?\"";
|
||||||
|
bashCheckProcess.StartInfo.RedirectStandardOutput = true;
|
||||||
|
bashCheckProcess.StartInfo.RedirectStandardError = true;
|
||||||
|
bashCheckProcess.StartInfo.UseShellExecute = false;
|
||||||
|
|
||||||
|
bashCheckProcess.Start();
|
||||||
|
string bashCheckOutput = bashCheckProcess.StandardOutput.ReadToEnd();
|
||||||
|
string bashCheckErrors = bashCheckProcess.StandardError.ReadToEnd();
|
||||||
|
bashCheckProcess.WaitForExit();
|
||||||
|
|
||||||
// Act - Check syntax using bash -n
|
// Act - Check syntax using bash -n
|
||||||
var process = new Process();
|
var process = new Process();
|
||||||
process.StartInfo.FileName = "bash";
|
process.StartInfo.FileName = "bash";
|
||||||
process.StartInfo.Arguments = $"-n {tempScriptPath}";
|
process.StartInfo.Arguments = $"-n {tempScriptPath}";
|
||||||
process.StartInfo.RedirectStandardError = true;
|
process.StartInfo.RedirectStandardError = true;
|
||||||
process.StartInfo.UseShellExecute = false;
|
process.StartInfo.UseShellExecute = false;
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
string errors = process.StandardError.ReadToEnd();
|
string errors = process.StandardError.ReadToEnd();
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(errors))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Errors: {errors}");
|
||||||
|
}
|
||||||
|
|
||||||
// Assert based on expected outcome
|
// Assert based on expected outcome
|
||||||
if (shouldPass)
|
if (shouldPass)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("Test expected to pass, checking exit code and errors");
|
||||||
Assert.Equal(0, process.ExitCode);
|
Assert.Equal(0, process.ExitCode);
|
||||||
Assert.Empty(errors);
|
Assert.Empty(errors);
|
||||||
|
|
||||||
|
if (shouldPass && process.ExitCode == 0 && useShellCheck)
|
||||||
|
{
|
||||||
|
RunShellCheck(tempScriptPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("Test expected to fail, checking exit code and errors");
|
||||||
Assert.NotEqual(0, process.ExitCode);
|
Assert.NotEqual(0, process.ExitCode);
|
||||||
Assert.NotEmpty(errors);
|
Assert.NotEmpty(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup - But leave the temp directory for debugging on failure
|
||||||
try
|
if (process.ExitCode == 0 && shouldPass)
|
||||||
{
|
{
|
||||||
Directory.Delete(tempDir, true);
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort cleanup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
// Best effort cleanup
|
Console.WriteLine($"Not cleaning up temp directory for debugging: {tempDir}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,10 +130,54 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to replace common placeholders in shell script templates
|
private void RunShellCheck(string scriptPath)
|
||||||
|
{
|
||||||
|
var shellcheckExistsProcess = new Process();
|
||||||
|
shellcheckExistsProcess.StartInfo.FileName = "which";
|
||||||
|
shellcheckExistsProcess.StartInfo.Arguments = "shellcheck";
|
||||||
|
shellcheckExistsProcess.StartInfo.RedirectStandardOutput = true;
|
||||||
|
shellcheckExistsProcess.StartInfo.UseShellExecute = false;
|
||||||
|
|
||||||
|
shellcheckExistsProcess.Start();
|
||||||
|
string shellcheckPath = shellcheckExistsProcess.StandardOutput.ReadToEnd().Trim();
|
||||||
|
shellcheckExistsProcess.WaitForExit();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(shellcheckPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("ShellCheck found, performing additional validation");
|
||||||
|
|
||||||
|
var shellcheckProcess = new Process();
|
||||||
|
shellcheckProcess.StartInfo.FileName = "shellcheck";
|
||||||
|
shellcheckProcess.StartInfo.Arguments = $"-e SC2001,SC2002,SC2006,SC2009,SC2016,SC2034,SC2039,SC2046,SC2048,SC2059,SC2086,SC2094,SC2115,SC2116,SC2126,SC2129,SC2140,SC2145,SC2153,SC2154,SC2155,SC2162,SC2164,SC2166,SC2174,SC2181,SC2206,SC2207,SC2221,SC2222,SC2230,SC2236,SC2242,SC2268 {scriptPath}";
|
||||||
|
shellcheckProcess.StartInfo.RedirectStandardOutput = true;
|
||||||
|
shellcheckProcess.StartInfo.RedirectStandardError = true;
|
||||||
|
shellcheckProcess.StartInfo.UseShellExecute = false;
|
||||||
|
|
||||||
|
shellcheckProcess.Start();
|
||||||
|
string shellcheckOutput = shellcheckProcess.StandardOutput.ReadToEnd();
|
||||||
|
string shellcheckErrors = shellcheckProcess.StandardError.ReadToEnd();
|
||||||
|
shellcheckProcess.WaitForExit();
|
||||||
|
|
||||||
|
if (shellcheckProcess.ExitCode != 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"ShellCheck found syntax errors: {shellcheckOutput}");
|
||||||
|
Console.WriteLine($"ShellCheck errors: {shellcheckErrors}");
|
||||||
|
|
||||||
|
Assert.Fail($"ShellCheck validation failed with exit code {shellcheckProcess.ExitCode}. Output: {shellcheckOutput}. Errors: {shellcheckErrors}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("ShellCheck validation passed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("ShellCheck not found, skipping additional validation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
|
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
|
||||||
{
|
{
|
||||||
// Replace common placeholders
|
|
||||||
template = template.Replace("_PROCESS_ID_", "1234");
|
template = template.Replace("_PROCESS_ID_", "1234");
|
||||||
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
|
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
|
||||||
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
|
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
|
||||||
@@ -117,34 +200,20 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("SkipOn", "windows")]
|
[Trait("SkipOn", "windows")]
|
||||||
public void UpdateShTemplateHasValidSyntax()
|
public void UpdateShTemplateHasValidSyntax()
|
||||||
{
|
{
|
||||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
}
|
{
|
||||||
|
return;
|
||||||
[Fact]
|
}
|
||||||
[Trait("Level", "L0")]
|
|
||||||
[Trait("Category", "Runner")]
|
try
|
||||||
[Trait("SkipOn", "windows")]
|
{
|
||||||
public void UpdateShTemplateWithErrorsFailsValidation()
|
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
|
||||||
{
|
}
|
||||||
ValidateShellScriptTemplateSyntax(
|
catch (Exception ex)
|
||||||
"src/Misc/layoutbin",
|
{
|
||||||
"update.sh.template",
|
Console.WriteLine($"Error during test: {ex}");
|
||||||
shouldPass: false,
|
throw;
|
||||||
templateModifier: template =>
|
}
|
||||||
{
|
|
||||||
// Introduce syntax errors
|
|
||||||
|
|
||||||
// 1. Missing 'fi' for an 'if' statement
|
|
||||||
template = template.Replace("fi\n", "\n");
|
|
||||||
|
|
||||||
// 2. Unbalanced quotes
|
|
||||||
template = template.Replace("date \"+[%F %T-%4N]", "date \"+[%F %T-%4N");
|
|
||||||
|
|
||||||
// 3. Invalid syntax in if condition
|
|
||||||
template = template.Replace("if [ $? -ne 0 ]", "if [ $? -ne 0");
|
|
||||||
|
|
||||||
return template;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -155,6 +224,27 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
{
|
{
|
||||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "darwin.svc.sh.template");
|
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "darwin.svc.sh.template");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "windows")]
|
||||||
|
public void DarwinSvcShTemplateWithErrorsFailsValidation()
|
||||||
|
{
|
||||||
|
ValidateShellScriptTemplateSyntax(
|
||||||
|
"src/Misc/layoutbin",
|
||||||
|
"darwin.svc.sh.template",
|
||||||
|
shouldPass: false,
|
||||||
|
templateModifier: template =>
|
||||||
|
{
|
||||||
|
|
||||||
|
template = template.Replace("fi\n", "\n");
|
||||||
|
template = template.Replace("esac", "");
|
||||||
|
template = template.Replace("\"$svcuser\"", "\"$svcuser");
|
||||||
|
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
@@ -164,6 +254,26 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
{
|
{
|
||||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.template");
|
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.template");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "windows")]
|
||||||
|
public void SystemdSvcShTemplateWithErrorsFailsValidation()
|
||||||
|
{
|
||||||
|
ValidateShellScriptTemplateSyntax(
|
||||||
|
"src/Misc/layoutbin",
|
||||||
|
"systemd.svc.sh.template",
|
||||||
|
shouldPass: false,
|
||||||
|
templateModifier: template =>
|
||||||
|
{
|
||||||
|
template = template.Replace("done\n", "\n");
|
||||||
|
template = template.Replace("function", "function (");
|
||||||
|
template = template.Replace("if [ ! -f ", "if ! -f ");
|
||||||
|
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
@@ -178,72 +288,108 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
[Trait("SkipOn", "windows")]
|
[Trait("SkipOn", "windows")]
|
||||||
public void UpdateShTemplateHasCorrectVariableReferencesAndIfStructure()
|
public void RunHelperShTemplateWithErrorsFailsValidation()
|
||||||
|
{
|
||||||
|
ValidateShellScriptTemplateSyntax(
|
||||||
|
"src/Misc/layoutroot",
|
||||||
|
"run-helper.sh.template",
|
||||||
|
shouldPass: false,
|
||||||
|
templateModifier: template =>
|
||||||
|
{
|
||||||
|
|
||||||
|
template = template.Replace("${RUNNER_ROOT}", "${RUNNER_ROOT");
|
||||||
|
template = template.Replace("\"$@\"", "\"$@");
|
||||||
|
template = template.Replace("> /dev/null", ">> >>");
|
||||||
|
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "windows")]
|
||||||
|
public void ValidateShellScript_MissingTemplate_ThrowsException()
|
||||||
{
|
{
|
||||||
// Skip on Windows
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "non_existent_template.sh.template", shouldPass: true);
|
||||||
{
|
Assert.Fail("Expected exception was not thrown");
|
||||||
// Arrange
|
|
||||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
|
||||||
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "update.sh.template");
|
|
||||||
|
|
||||||
// Read the template
|
|
||||||
string template = File.ReadAllText(templatePath);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
// 1. Check that $restartinteractiverunner is correctly referenced with $ in if condition
|
|
||||||
Assert.Contains("if [[ \"$currentplatform\" == 'darwin' && $restartinteractiverunner -eq 0 ]];\nthen", template);
|
|
||||||
|
|
||||||
// 2. Check for proper nesting of if statements for node version checks
|
|
||||||
int nodeVersionCheckLines = 0;
|
|
||||||
bool foundNode24Block = false;
|
|
||||||
bool foundNode16Block = false;
|
|
||||||
bool foundNode12Block = false;
|
|
||||||
bool hasProperIndentation = false;
|
|
||||||
|
|
||||||
string[] lines = template.Split('\n');
|
|
||||||
for (int i = 0; i < lines.Length; i++)
|
|
||||||
{
|
|
||||||
string line = lines[i];
|
|
||||||
if (line.Contains("nodever=\"node24\""))
|
|
||||||
{
|
|
||||||
foundNode24Block = true;
|
|
||||||
}
|
|
||||||
if (line.Contains("nodever=\"node16\""))
|
|
||||||
{
|
|
||||||
foundNode16Block = true;
|
|
||||||
}
|
|
||||||
if (foundNode16Block && line.Contains("nodever=\"node12\""))
|
|
||||||
{
|
|
||||||
foundNode12Block = true;
|
|
||||||
// Check if we have proper indentation for this nested block
|
|
||||||
hasProperIndentation = line.StartsWith(" ");
|
|
||||||
}
|
|
||||||
if (line.Contains("Fallback if RunnerService.js was started with"))
|
|
||||||
{
|
|
||||||
nodeVersionCheckLines++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The template has node24 check but there's no "Fallback if RunnerService.js was started with node24" comment for it
|
|
||||||
// Only the node20, node16, and node12 sections have this comment
|
|
||||||
Assert.Equal(3, nodeVersionCheckLines); // node20, node16, node12
|
|
||||||
Assert.True(foundNode24Block, "Could not find node24 block");
|
|
||||||
Assert.True(foundNode16Block, "Could not find node16 block");
|
|
||||||
Assert.True(foundNode12Block, "Could not find node12 block");
|
|
||||||
Assert.True(hasProperIndentation, "node12 block is not properly indented");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Assert.Fail($"Exception during test: {ex.ToString()}");
|
Assert.Contains("non_existent_template.sh.template", ex.Message);
|
||||||
|
Assert.Contains("FileNotFoundException", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "windows")]
|
||||||
|
public void ValidateShellScript_ComplexScript_ValidatesCorrectly()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test template with complex shell scripting patterns
|
||||||
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
string templatePath = Path.Combine(tempDir, "complex_shell.sh.template");
|
||||||
|
|
||||||
|
// Write a sample template with various shell features
|
||||||
|
string template = @"#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Function with nested quotes and complex syntax
|
||||||
|
function complex_func() {
|
||||||
|
local var1=""$1""
|
||||||
|
local var2=""${2:-default}""
|
||||||
|
echo ""Function arguments: '$var1' and '$var2'""
|
||||||
|
if [ ""$var1"" == ""test"" ]; then
|
||||||
|
echo ""This is a 'test' with nested quotes""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Complex variable substitutions
|
||||||
|
VAR1=""test value""
|
||||||
|
VAR2=""${VAR1:0:4}""
|
||||||
|
VAR3=""$(echo ""command substitution"")""
|
||||||
|
|
||||||
|
# Here document
|
||||||
|
cat << EOF > /tmp/testfile
|
||||||
|
This is a test file
|
||||||
|
With multiple lines
|
||||||
|
And some $VAR1 substitution
|
||||||
|
EOF
|
||||||
|
|
||||||
|
complex_func ""test"" ""value""
|
||||||
|
exit 0";
|
||||||
|
|
||||||
|
File.WriteAllText(templatePath, template);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ValidateShellScriptTemplateSyntax("", templatePath, shouldPass: true, useFullPath: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Clean up
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort cleanup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +414,6 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("SkipOn", "osx,linux")]
|
[Trait("SkipOn", "osx,linux")]
|
||||||
public void UpdateCmdTemplateWithErrorsFailsValidation()
|
public void UpdateCmdTemplateWithErrorsFailsValidation()
|
||||||
{
|
{
|
||||||
// Skip on non-Windows platforms
|
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -277,87 +422,347 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
|
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
|
||||||
templateModifier: template =>
|
templateModifier: template =>
|
||||||
{
|
{
|
||||||
// Introduce syntax errors in the template
|
|
||||||
// 1. Unbalanced parentheses
|
|
||||||
template = template.Replace("if exist", "if exist (");
|
template = template.Replace("if exist", "if exist (");
|
||||||
|
|
||||||
// 2. Unclosed quotes
|
|
||||||
template = template.Replace("echo", "echo \"Unclosed quote");
|
template = template.Replace("echo", "echo \"Unclosed quote");
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "osx,linux")]
|
||||||
|
public void ValidateCmdScript_MissingTemplate_ThrowsFileNotFoundException()
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||||
|
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "non_existent_template.cmd.template");
|
||||||
|
string content = File.ReadAllText(templatePath);
|
||||||
|
|
||||||
|
Assert.Fail($"Expected FileNotFoundException was not thrown for {templatePath}");
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// This is expected, so test passes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "osx,linux")]
|
||||||
|
public void ValidateCmdScript_ComplexQuoting_ValidatesCorrectly()
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
string templatePath = Path.Combine(tempDir, "complex_quotes.cmd.template");
|
||||||
|
|
||||||
|
string template = @"@echo off
|
||||||
|
echo ""This has ""nested"" quotes""
|
||||||
|
echo ""This has an escaped quote: \""test\""""
|
||||||
|
echo Simple command
|
||||||
|
if ""quoted condition"" == ""quoted condition"" (
|
||||||
|
echo ""Inside if block with quotes""
|
||||||
|
)";
|
||||||
|
|
||||||
|
File.WriteAllText(templatePath, template);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Clean up
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
[Trait("SkipOn", "osx,linux")]
|
||||||
|
public void ValidateCmdScript_ComplexParentheses_ValidatesCorrectly()
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
string templatePath = Path.Combine(tempDir, "complex_parens.cmd.template");
|
||||||
|
|
||||||
|
string template = @"@echo off
|
||||||
|
echo Text with (parentheses)
|
||||||
|
echo ""Text with (parentheses inside quotes)""
|
||||||
|
if exist file.txt (
|
||||||
|
if exist other.txt (
|
||||||
|
echo Nested if blocks
|
||||||
|
) else (
|
||||||
|
echo Nested else
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo Outer else
|
||||||
|
)";
|
||||||
|
|
||||||
|
File.WriteAllText(templatePath, template);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null)
|
private bool HasUnclosedQuotes(string text)
|
||||||
|
{
|
||||||
|
bool inQuote = false;
|
||||||
|
bool isEscaped = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char c = text[i];
|
||||||
|
|
||||||
|
if (c == '\\')
|
||||||
|
{
|
||||||
|
isEscaped = !isEscaped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"' && !isEscaped)
|
||||||
|
{
|
||||||
|
inQuote = !inQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c != '\\')
|
||||||
|
{
|
||||||
|
isEscaped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasBalancedParentheses(string text)
|
||||||
|
{
|
||||||
|
int balance = 0;
|
||||||
|
bool inQuote = false;
|
||||||
|
bool isEscaped = false;
|
||||||
|
bool inComment = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char c = text[i];
|
||||||
|
|
||||||
|
if (inComment)
|
||||||
|
{
|
||||||
|
if (c == '\n' || c == '\r')
|
||||||
|
{
|
||||||
|
inComment = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inQuote && i < text.Length - 1 && c == ':' && text[i+1] == ':')
|
||||||
|
{
|
||||||
|
inComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inQuote && i < text.Length - 2 && c == 'r' && text[i+1] == 'e' && text[i+2] == 'm' &&
|
||||||
|
(i == 0 || char.IsWhiteSpace(text[i-1])))
|
||||||
|
{
|
||||||
|
inComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '\\')
|
||||||
|
{
|
||||||
|
isEscaped = !isEscaped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '"' && !isEscaped)
|
||||||
|
{
|
||||||
|
inQuote = !inQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inQuote)
|
||||||
|
{
|
||||||
|
if (c == '(')
|
||||||
|
{
|
||||||
|
balance++;
|
||||||
|
}
|
||||||
|
else if (c == ')')
|
||||||
|
{
|
||||||
|
balance--;
|
||||||
|
if (balance < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c != '\\')
|
||||||
|
{
|
||||||
|
isEscaped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return balance == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null, bool useFullPath = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
string templatePath;
|
||||||
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
|
|
||||||
|
if (useFullPath)
|
||||||
|
{
|
||||||
|
templatePath = templateName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||||
|
templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
|
||||||
|
}
|
||||||
|
|
||||||
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
Directory.CreateDirectory(tempDir);
|
Directory.CreateDirectory(tempDir);
|
||||||
string tempUpdatePath = Path.Combine(tempDir, Path.GetFileName(templateName).Replace(".template", ""));
|
string tempUpdatePath = Path.Combine(tempDir, Path.GetFileName(templatePath).Replace(".template", ""));
|
||||||
|
|
||||||
// Read the template
|
|
||||||
string template = File.ReadAllText(templatePath);
|
string template = File.ReadAllText(templatePath);
|
||||||
|
|
||||||
// Apply template modifier if provided (for injecting errors)
|
|
||||||
if (templateModifier != null)
|
if (templateModifier != null)
|
||||||
{
|
{
|
||||||
template = templateModifier(template);
|
template = templateModifier(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the placeholders with valid test values
|
|
||||||
template = template.Replace("_PROCESS_ID_", "1234");
|
template = template.Replace("_PROCESS_ID_", "1234");
|
||||||
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
|
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
|
||||||
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
|
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||||
|
template = template.Replace("_ROOT_FOLDER_", rootFolder);
|
||||||
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
|
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
|
||||||
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
|
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
|
||||||
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
|
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
|
||||||
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
|
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
|
||||||
|
|
||||||
// Write the processed template to a temporary file
|
|
||||||
File.WriteAllText(tempUpdatePath, template);
|
File.WriteAllText(tempUpdatePath, template);
|
||||||
|
|
||||||
// Act - Check syntax using cmd with special flags:
|
|
||||||
// /v:on - Enable delayed environment variable expansion
|
string errors = string.Empty;
|
||||||
// /f:off - Disable file name completion
|
string output = string.Empty;
|
||||||
// /e:on - Enable command extensions
|
int exitCode = 0;
|
||||||
// These flags help validate the syntax without fully executing the script
|
|
||||||
var process = new Process();
|
|
||||||
process.StartInfo.FileName = "cmd.exe";
|
|
||||||
process.StartInfo.Arguments = $"/c /v:on /f:off /e:on \"{tempUpdatePath}\" echo SyntaxCheckOnly && exit /b 0";
|
|
||||||
process.StartInfo.RedirectStandardError = true;
|
|
||||||
process.StartInfo.RedirectStandardOutput = true;
|
|
||||||
process.StartInfo.UseShellExecute = false;
|
|
||||||
process.Start();
|
|
||||||
string errors = process.StandardError.ReadToEnd();
|
|
||||||
string output = process.StandardOutput.ReadToEnd();
|
|
||||||
process.WaitForExit();
|
|
||||||
|
|
||||||
// Check for mismatched parentheses in the file content
|
try
|
||||||
int openParenCount = template.Split('(').Length - 1;
|
{
|
||||||
int closeParenCount = template.Split(')').Length - 1;
|
string testBatchFile = Path.Combine(tempDir, "test.cmd");
|
||||||
bool hasMissingParenthesis = openParenCount != closeParenCount;
|
File.WriteAllText(testBatchFile, "@echo off\r\nexit /b 0");
|
||||||
|
|
||||||
|
var process = new Process();
|
||||||
|
process.StartInfo.FileName = "cmd.exe";
|
||||||
|
process.StartInfo.Arguments = $"/c \"cd /d \"{tempDir}\" && echo Script syntax check && exit /b 0\"";
|
||||||
|
process.StartInfo.RedirectStandardError = true;
|
||||||
|
process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
process.StartInfo.UseShellExecute = false;
|
||||||
|
process.StartInfo.WorkingDirectory = tempDir;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
output = process.StandardOutput.ReadToEnd();
|
||||||
|
errors = process.StandardError.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
exitCode = process.ExitCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors = ex.ToString();
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for unclosed quotes (simple check - not perfect but catches obvious errors)
|
bool hasMissingParenthesis = !HasBalancedParentheses(template);
|
||||||
int doubleQuoteCount = template.Split('"').Length - 1;
|
bool hasUnclosedQuotes = HasUnclosedQuotes(template);
|
||||||
bool hasUnclosedQuotes = doubleQuoteCount % 2 != 0;
|
|
||||||
|
|
||||||
// Determine if the validation passed
|
bool hasOutputErrors = !string.IsNullOrEmpty(errors) ||
|
||||||
bool validationPassed = process.ExitCode == 0 &&
|
output.Contains("syntax error") ||
|
||||||
string.IsNullOrEmpty(errors) &&
|
output.Contains("not recognized") ||
|
||||||
!hasMissingParenthesis &&
|
output.Contains("unexpected") ||
|
||||||
!hasUnclosedQuotes;
|
output.Contains("Syntax check failed");
|
||||||
|
|
||||||
|
bool hasInvalidSyntaxPatterns = false;
|
||||||
|
|
||||||
|
if (template.Contains("if") && !template.Contains("if "))
|
||||||
|
{
|
||||||
|
hasInvalidSyntaxPatterns = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.Contains("goto") && !template.Contains("goto "))
|
||||||
|
{
|
||||||
|
hasInvalidSyntaxPatterns = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.Contains("(") && !template.Contains(")"))
|
||||||
|
{
|
||||||
|
hasInvalidSyntaxPatterns = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool staticAnalysisPassed = !hasMissingParenthesis &&
|
||||||
|
!hasUnclosedQuotes &&
|
||||||
|
!hasInvalidSyntaxPatterns;
|
||||||
|
|
||||||
|
bool executionPassed = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!errors.Contains("filename, directory name, or volume label syntax"))
|
||||||
|
{
|
||||||
|
executionPassed = exitCode == 0 && !hasOutputErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
executionPassed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validationPassed = staticAnalysisPassed && executionPassed;
|
||||||
|
|
||||||
// Assert based on expected outcome
|
|
||||||
if (shouldPass)
|
if (shouldPass)
|
||||||
{
|
{
|
||||||
Assert.True(validationPassed,
|
Assert.True(validationPassed,
|
||||||
$"Template validation should have passed but failed. Exit code: {process.ExitCode}, " +
|
$"Template validation should have passed but failed. Exit code: {exitCode}, " +
|
||||||
$"Errors: {errors}, HasMissingParenthesis: {hasMissingParenthesis}, " +
|
$"Errors: {errors}, HasMissingParenthesis: {hasMissingParenthesis}, " +
|
||||||
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
|
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user