Compare commits

..

10 Commits

Author SHA1 Message Date
Salman Chishti
820bcaf258 Update generic methdod to shelcheck all files that can be 2025-07-29 07:18:21 +00:00
Salman Chishti
1f9418d6c0 update 2025-07-28 13:08:39 +00:00
Salman Chishti
49b30c8a23 add skip back for windows 2025-07-28 13:01:04 +00:00
Salman Chishti
577c73ee80 don't skip on windows 2025-07-28 12:34:13 +00:00
Salman Chishti
2dc3c3adc1 Enhance shell script validation with detailed debugging and ShellCheck integration 2025-07-28 12:08:46 +00:00
Salman Chishti
ffc2086972 skip win 64 2025-07-27 22:07:16 +00:00
Salman Chishti
63ada10762 update 2025-07-27 21:54:57 +00:00
Salman Chishti
c824407a9b better validations 2025-07-27 21:48:52 +00:00
Salman Chishti
f06a45283d update with temp file 2025-07-27 21:38:44 +00:00
Salman Chishti
3aef228adc Add tests for making sure that template files have no syntax errors 2025-07-27 21:24:45 +00:00
2 changed files with 552 additions and 147 deletions

View File

@@ -2,6 +2,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Sdk;
using Xunit;
@@ -10,8 +11,7 @@ namespace GitHub.Runner.Common.Tests.Listener
{
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)
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null, bool useFullPath = false, bool useShellCheck = true)
{
// Skip on Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -24,57 +24,91 @@ namespace GitHub.Runner.Common.Tests.Listener
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string templatePath = Path.Combine(rootDirectory, relativePath, templateName);
templatePath = Path.Combine(rootDirectory, relativePath, templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
string debugLogPath = Path.Combine(tempDir, "debug_log.txt");
// Read the template
string template = File.ReadAllText(templatePath);
// Apply template modifier if provided (for injecting errors)
if (templateModifier != null)
{
template = templateModifier(template);
}
// Replace common placeholders with valid test values
template = ReplaceCommonPlaceholders(template, rootDirectory, tempDir);
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
template = ReplaceCommonPlaceholders(template, rootFolder, tempDir);
// Write the processed template to a temporary file
File.WriteAllText(tempScriptPath, template);
// Make the file executable
var chmodProcess = new Process();
chmodProcess.StartInfo.FileName = "chmod";
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
chmodProcess.Start();
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
var process = new Process();
process.StartInfo.FileName = "bash";
process.StartInfo.Arguments = $"-n {tempScriptPath}";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.Start();
string errors = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(errors))
{
Console.WriteLine($"Errors: {errors}");
}
// Assert based on expected outcome
if (shouldPass)
{
Console.WriteLine("Test expected to pass, checking exit code and errors");
Assert.Equal(0, process.ExitCode);
Assert.Empty(errors);
if (shouldPass && process.ExitCode == 0 && useShellCheck)
{
RunShellCheck(tempScriptPath);
}
}
else
{
Console.WriteLine("Test expected to fail, checking exit code and errors");
Assert.NotEqual(0, process.ExitCode);
Assert.NotEmpty(errors);
}
// Cleanup
// Cleanup - But leave the temp directory for debugging on failure
if (process.ExitCode == 0 && shouldPass)
{
try
{
Directory.Delete(tempDir, true);
@@ -84,6 +118,11 @@ namespace GitHub.Runner.Common.Tests.Listener
// Best effort cleanup
}
}
else
{
Console.WriteLine($"Not cleaning up temp directory for debugging: {tempDir}");
}
}
}
catch (Exception ex)
{
@@ -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)
{
// Replace common placeholders
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
@@ -117,34 +200,20 @@ namespace GitHub.Runner.Common.Tests.Listener
[Trait("SkipOn", "windows")]
public void UpdateShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void UpdateShTemplateWithErrorsFailsValidation()
try
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"update.sh.template",
shouldPass: false,
templateModifier: template =>
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
}
catch (Exception ex)
{
// 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;
});
Console.WriteLine($"Error during test: {ex}");
throw;
}
}
[Fact]
@@ -156,6 +225,27 @@ namespace GitHub.Runner.Common.Tests.Listener
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]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
@@ -165,6 +255,26 @@ namespace GitHub.Runner.Common.Tests.Listener
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]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
@@ -178,9 +288,29 @@ namespace GitHub.Runner.Common.Tests.Listener
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[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))
{
return;
@@ -188,62 +318,78 @@ namespace GitHub.Runner.Common.Tests.Listener
try
{
using (var hc = new TestHostContext(this))
{
// 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");
}
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "non_existent_template.sh.template", shouldPass: true);
Assert.Fail("Expected exception was not thrown");
}
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")]
public void UpdateCmdTemplateWithErrorsFailsValidation()
{
// Skip on non-Windows platforms
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
@@ -277,87 +422,347 @@ namespace GitHub.Runner.Common.Tests.Listener
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
templateModifier: template =>
{
// Introduce syntax errors in the template
// 1. Unbalanced parentheses
template = template.Replace("if exist", "if exist (");
// 2. Unclosed quotes
template = template.Replace("echo", "echo \"Unclosed quote");
return template;
});
}
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null)
[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 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
{
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
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);
// Apply template modifier if provided (for injecting errors)
if (templateModifier != null)
{
template = templateModifier(template);
}
// Replace the placeholders with valid test values
template = template.Replace("_PROCESS_ID_", "1234");
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("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
// Write the processed template to a temporary file
File.WriteAllText(tempUpdatePath, template);
// Act - Check syntax using cmd with special flags:
// /v:on - Enable delayed environment variable expansion
// /f:off - Disable file name completion
// /e:on - Enable command extensions
// These flags help validate the syntax without fully executing the script
string errors = string.Empty;
string output = string.Empty;
int exitCode = 0;
try
{
string testBatchFile = Path.Combine(tempDir, "test.cmd");
File.WriteAllText(testBatchFile, "@echo off\r\nexit /b 0");
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.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();
string errors = process.StandardError.ReadToEnd();
string output = process.StandardOutput.ReadToEnd();
output = process.StandardOutput.ReadToEnd();
errors = process.StandardError.ReadToEnd();
process.WaitForExit();
exitCode = process.ExitCode;
}
catch (Exception ex)
{
errors = ex.ToString();
exitCode = 1;
}
// Check for mismatched parentheses in the file content
int openParenCount = template.Split('(').Length - 1;
int closeParenCount = template.Split(')').Length - 1;
bool hasMissingParenthesis = openParenCount != closeParenCount;
bool hasMissingParenthesis = !HasBalancedParentheses(template);
bool hasUnclosedQuotes = HasUnclosedQuotes(template);
// Check for unclosed quotes (simple check - not perfect but catches obvious errors)
int doubleQuoteCount = template.Split('"').Length - 1;
bool hasUnclosedQuotes = doubleQuoteCount % 2 != 0;
bool hasOutputErrors = !string.IsNullOrEmpty(errors) ||
output.Contains("syntax error") ||
output.Contains("not recognized") ||
output.Contains("unexpected") ||
output.Contains("Syntax check failed");
// Determine if the validation passed
bool validationPassed = process.ExitCode == 0 &&
string.IsNullOrEmpty(errors) &&
!hasMissingParenthesis &&
!hasUnclosedQuotes;
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)
{
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}, " +
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
}