diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 8219c0f..b330bad 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -5,9 +5,7 @@ name: compile and test
on:
push:
- branches: [ "main" ]
pull_request:
- branches: [ "main" ]
jobs:
test:
diff --git a/.gitignore b/.gitignore
index 36bd81a..62534d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@ obj/
Playground/
/MSBuildWasmLocal.sln
/examples/go
-templates/src
\ No newline at end of file
+templates/src
+/test/WasmTasksTests/GeneratedRustTests
diff --git a/src/WasmTask.cs b/src/WasmTask.cs
index aae5012..cb27609 100644
--- a/src/WasmTask.cs
+++ b/src/WasmTask.cs
@@ -301,11 +301,11 @@ private void ReflectOutputJsonToClassProperties(string taskOutputJson)
}
catch (JsonException ex)
{
- Log.LogError($"Error parsing JSON: {ex.Message}");
+ Log.LogError($"Error parsing output JSON: {ex.Message}");
}
catch (Exception ex)
{
- Log.LogError($"Error Reflecting properties from Json to Class: {ex.Message}");
+ Log.LogError($"Error Reflecting properties from Json to Class after task run: {ex.Message}");
}
}
diff --git a/src/WasmTaskFactory.cs b/src/WasmTaskFactory.cs
index b48614f..b84cb4d 100644
--- a/src/WasmTaskFactory.cs
+++ b/src/WasmTaskFactory.cs
@@ -15,10 +15,12 @@ namespace MSBuildWasm
///
public class WasmTaskFactory : ITaskFactory2
{
- // TODO avoid hardcoded when possible
+ ///
+ /// Name displayed in the MSBuild log.
+ ///
public string FactoryName => nameof(WasmTaskFactory);
+ public TaskLoggingHelper Log { get; set; }
private TaskPropertyInfo[] _taskProperties;
- private TaskLoggingHelper _log;
private bool _taskInfoReceived = false;
private string _taskName;
private string _taskPath;
@@ -45,7 +47,7 @@ public TaskPropertyInfo[] GetTaskParameters()
public bool Initialize(string taskName, IDictionary factoryIdentityParameters, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost)
{
- _log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName)
+ Log = new TaskLoggingHelper(taskFactoryLoggingHost, taskName)
{
HelpKeywordPrefix = $"WasmTask.{taskName}."
};
@@ -74,7 +76,7 @@ private void GetCustomWasmTaskProperties()
{
using var engine = new Engine();
using var module = Wasmtime.Module.FromFile(engine, _taskPath);
- using var linker = new WasmTaskLinker(engine, _log);
+ using var linker = new WasmTaskLinker(engine, Log);
using var store = new Store(engine);
linker.DefineWasi();
linker.LinkLogFunctions(store);
@@ -84,7 +86,7 @@ private void GetCustomWasmTaskProperties()
Action getTaskInfo = instance.GetAction(WasmTask.GetTaskInfoFunctionName);
if (getTaskInfo == null)
{
- _log.LogError("Function 'GetTaskInfo' not found in the WebAssembly module.");
+ Log.LogError("Function 'GetTaskInfo' not found in the WebAssembly module.");
return;
}
@@ -92,11 +94,11 @@ private void GetCustomWasmTaskProperties()
}
catch (WasmtimeException ex)
{
- _log.LogErrorFromException(ex, true);
+ Log.LogErrorFromException(ex, true);
}
if (!_taskInfoReceived)
{
- _log.LogError("Task info was not received from the WebAssembly module.");
+ Log.LogError("Task info was not received from the WebAssembly module.");
}
}
@@ -104,15 +106,19 @@ private void GetCustomWasmTaskProperties()
/// Handling callback with Task Info JSON.
///
/// WASIp2: get structured info from GetTaskInfo function output and parse it via classes from wit-bindgen and convert them to properties, this event scheme unnecessary
- private void OnTaskInfoReceived(object sender, string taskInfoJson)
+ internal void OnTaskInfoReceived(object sender, string taskInfoJson)
{
try
{
_taskProperties = Serializer.DeserializeTaskInfoJson(taskInfoJson);
}
- catch (Exception ex) when (ex is JsonException || ex is KeyNotFoundException || ex is ArgumentException)
+ catch (Exception ex) when (ex is JsonException || ex is KeyNotFoundException || ex is ArgumentException || ex is InvalidOperationException)
+ {
+ Log.LogError("Could not deserialize Task Info JSON. {0}:{1}", ex.GetType().ToString(), ex.Message);
+ }
+ catch (Exception ex)
{
- _log.LogErrorFromException(ex);
+ Log.LogError("Unknown error in Task Info JSON deserialization. {0}", ex.Message);
}
_taskInfoReceived = true;
}
diff --git a/test/WasmTasksTests/WasmTaskFactory_Tests.cs b/test/WasmTasksTests/WasmTaskFactory_Tests.cs
index c6ce1d9..c1074ed 100644
--- a/test/WasmTasksTests/WasmTaskFactory_Tests.cs
+++ b/test/WasmTasksTests/WasmTaskFactory_Tests.cs
@@ -50,29 +50,47 @@ public void BuildTaskType_CreatesTypeWithCorrectProperties(Type propType)
Assert.NotNull(resultType.GetProperty(prop1name));
Assert.Null(resultType.GetProperty(prop2name));
}
- // split/theory
- [Fact]
- public void BuildTaskType_CreatesTypeWithCorrectAttributes()
+
+ [Theory]
+ [InlineData("StringProp", typeof(string), true, false)]
+ [InlineData("BoolProp", typeof(bool), false, true)]
+ [InlineData("TaskItemProp", typeof(ITaskItem), false, false)]
+ [InlineData("StringArrayProp", typeof(string[]), true, true)]
+ public void BuildTaskType_CreatesTypeWithCorrectAttributes(string propertyName, Type propertyType, bool isOutput, bool isRequired)
{
// Arrange
const string taskName = "TestTask";
- const string prop1name = "p1";
- const string prop2name = "p2";
TaskPropertyInfo[] properties = new[]
{
- new TaskPropertyInfo(prop1name, typeof(string), output: true, required: false),
- new TaskPropertyInfo(prop2name, typeof(bool), false, true)
- };
+ new TaskPropertyInfo(propertyName, propertyType, isOutput, isRequired)
+ };
// Act
Type resultType = WasmTaskReflectionBuilder.BuildTaskType(taskName, properties);
// Assert
- Assert.NotNull(resultType.GetProperty(prop1name)!.GetCustomAttribute());
- Assert.Null(resultType.GetProperty(prop1name)!.GetCustomAttribute());
- Assert.NotNull(resultType.GetProperty(prop2name)!.GetCustomAttribute());
- Assert.Null(resultType.GetProperty(prop2name)!.GetCustomAttribute());
+ PropertyInfo? property = resultType.GetProperty(propertyName);
+ Assert.NotNull(property);
+
+ if (isOutput)
+ {
+ Assert.NotNull(property!.GetCustomAttribute());
+ }
+ else
+ {
+ Assert.Null(property!.GetCustomAttribute());
+ }
+
+ if (isRequired)
+ {
+ Assert.NotNull(property!.GetCustomAttribute());
+ }
+ else
+ {
+ Assert.Null(property!.GetCustomAttribute());
+ }
}
+
[Fact]
public void ConvertJsonTaskInfoToProperties_ShouldParseProperties()
{
@@ -90,16 +108,21 @@ public void ConvertJsonTaskInfoToProperties_ShouldParseProperties()
propsExpected.ShouldBeEquivalentTo(propsParsed);
}
- // the task returns undeserializable json, should error
- //[Fact]
- //public void GetTaskInfo_InvalidJson_ShouldError()
- //{
- // const string invalidJson = "{ \"Properties\": { \"Dirs\": { \"type\": \"ITaskItem[]\", \"required\": true, \"output\": false }, \"MergedDir\": { \"type\": \"ITaskItem\", \"required\": false, \"output\": true }, \"MergedName\": { \"type\": \"string\", \"required\": false, \"output\": false } ";
+ [Fact]
+ public void GetTaskInfo_InvalidJson_ShouldError()
+ {
+ const string invalidJson = "{ \"Properties\": { \"Dirs\": { \"type\": \"ITaskItem[]\", \"required\": true, \"output\": false }, \"MergedDir\": { \"type\": \"ITaskItem\", \"required\": false, \"output\": true }, \"MergedName\": { \"type\": \"string\", \"required\": false, \"output\": false {} ";
- // WasmTaskFactory factory = new WasmTaskFactory();
+ MockEngine m = new MockEngine();
- // factory.OnTaskInfoReceived(null, invalidJson);
+ WasmTaskFactory factory = new WasmTaskFactory
+ {
+ Log = new Microsoft.Build.Utilities.TaskLoggingHelper(m, "TestTask")
+ };
+ factory.OnTaskInfoReceived(null, invalidJson);
+ m.AssertLogContains("Error");
+ }
diff --git a/test/WasmTasksTests/WasmTask_Tests.cs b/test/WasmTasksTests/WasmTask_Tests.cs
index 3aceed1..e72bdaa 100644
--- a/test/WasmTasksTests/WasmTask_Tests.cs
+++ b/test/WasmTasksTests/WasmTask_Tests.cs
@@ -13,6 +13,7 @@ namespace WasmTasksTests
public class WasmTask_Tests : IDisposable
{
private const string SOLUTION_ROOT_PATH = "../../../../../";
+ private const string TEST_PROJECT_PATH = "../../../";
private const string WASM_RUST_TARGET_PATH = "target/wasm32-wasi/release/";
private static readonly string[] s_names = ["rust_template", "rust_concat2files", "rust_mergedirectories"];
private static readonly string[] s_paths = [$"templates/content/RustWasmTaskTemplate/{s_names[0]}/", $"examples/{s_names[1]}/", $"examples/{s_names[2]}/"];
@@ -160,42 +161,220 @@ private static void ExecuteCommand(string command)
Console.WriteLine($"Exception: {ex.Message}");
}
}
- public class TemplateWasmTask : WasmTask
- {
- public TemplateWasmTask() : base()
+ public class TemplateWasmTask : WasmTask
{
- WasmFilePath = WasmTask_Tests.s_templateFilePath;
- BuildEngine = new MockEngine();
+ public TemplateWasmTask() : base()
+ {
+ WasmFilePath = WasmTask_Tests.s_templateFilePath;
+ BuildEngine = new MockEngine();
+ }
}
- }
- public class ConcatWasmTask : WasmTask
- {
- public ITaskItem? InputFile1 { get; set; }
- public ITaskItem? InputFile2 { get; set; }
- [Output]
- public ITaskItem? OutputFile { get; set; }
+ public class ConcatWasmTask : WasmTask
+ {
+ public ITaskItem? InputFile1 { get; set; }
+ public ITaskItem? InputFile2 { get; set; }
+ [Output]
+ public ITaskItem? OutputFile { get; set; }
- public ConcatWasmTask() : base()
+ public ConcatWasmTask() : base()
+ {
+ WasmFilePath = WasmTask_Tests.s_concatFilePath;
+ BuildEngine = new MockEngine();
+ }
+ }
+
+ public class DirectoryMergeWasmTask : WasmTask
{
- WasmFilePath = WasmTask_Tests.s_concatFilePath;
- BuildEngine = new MockEngine();
+ public ITaskItem[]? Dirs { get; set; }
+ public string? MergedName { get; set; }
+ [Output]
+ public ITaskItem? MergedDir { get; set; }
+
+ public DirectoryMergeWasmTask() : base()
+ {
+ WasmFilePath = WasmTask_Tests.s_mergeFilePath;
+ BuildEngine = new MockEngine();
+ }
}
- }
+ private class CustomRustE2ETest
+ {
+ private readonly string _name;
+ private readonly string _templatePath;
+ private readonly string _testProjectPath;
- public class DirectoryMergeWasmTask : WasmTask
- {
- public ITaskItem[]? Dirs { get; set; }
- public string? MergedName { get; set; }
- [Output]
- public ITaskItem? MergedDir { get; set; }
+ public CustomRustE2ETest(string name, string toplevel, string executeRustCode, string getTaskInfoRustCode)
+ {
+ _name = name;
+ _templatePath = Path.Combine(SOLUTION_ROOT_PATH, s_paths[0]);
+ _testProjectPath = Path.Combine(TEST_PROJECT_PATH, "GeneratedRustTests", _name);
+
+ CopyTemplate();
+ CreateLibRs(toplevel, executeRustCode, getTaskInfoRustCode);
+ Compile();
+ }
+
+ private void CopyTemplate()
+ {
+ if (Directory.Exists(_testProjectPath))
+ {
+ Directory.Delete(_testProjectPath, true);
+ }
+ DirectoryCopy(_templatePath, _testProjectPath, true);
+ }
+
+ private void CreateLibRs(string topLevel, string execute, string getTaskInfo)
+ {
+ string contents = $@"
+mod msbuild;
+use msbuild::logging::{{log_warning, log_error, log_message}};
+use msbuild::task_info::{{task_info, Property, PropertyType, TaskInfoStruct, TaskResult}};
+use serde::{{Serialize, Deserialize}};
+{topLevel}
+
+#[no_mangle]
+#[allow(non_snake_case)]
+pub fn Execute() -> TaskResult
+{{
+{execute}
+}}
+
+#[no_mangle]
+#[allow(non_snake_case)]
+pub fn GetTaskInfo()
+{{
+{getTaskInfo}
+}}
+";
+ File.WriteAllText(Path.Combine(_testProjectPath, "src", "lib.rs"), contents);
+ }
+
+ private void Compile()
+ {
+ string cargo_toml = Path.Combine(_testProjectPath, "Cargo.toml");
+ ExecuteCommand($"cargo build --release --target wasm32-wasi --manifest-path {cargo_toml}");
+ }
+
+ public string GetWasmFilePath()
+ {
+ return Path.Combine(_testProjectPath, WASM_RUST_TARGET_PATH, $"rust_template.wasm");
+ }
- public DirectoryMergeWasmTask() : base()
+ private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
+ {
+ DirectoryInfo dir = new DirectoryInfo(sourceDirName);
+ DirectoryInfo[] dirs = dir.GetDirectories();
+
+ if (!Directory.Exists(destDirName))
+ {
+ Directory.CreateDirectory(destDirName);
+ }
+
+ FileInfo[] files = dir.GetFiles();
+ foreach (FileInfo file in files)
+ {
+ string tempPath = Path.Combine(destDirName, file.Name);
+ file.CopyTo(tempPath, false);
+ }
+
+ if (copySubDirs)
+ {
+ foreach (DirectoryInfo subdir in dirs)
+ {
+ string tempPath = Path.Combine(destDirName, subdir.Name);
+ DirectoryCopy(subdir.FullName, tempPath, copySubDirs);
+ }
+ }
+ }
+ }
+
+ private class EmptyWasmTask : WasmTask
{
- WasmFilePath = WasmTask_Tests.s_mergeFilePath;
- BuildEngine = new MockEngine();
+ public EmptyWasmTask(string path) : base()
+ {
+ WasmFilePath = path;
+ BuildEngine = new MockEngine();
+ }
}
- }
+
+ // what happens if the rust tries to log invalid strings
+ // note that when there is an error in the test, the template .wasm is used leading to unclear messages
+ [Theory]
+ [InlineData("invmem_nullptr", "std::ptr::null()", "0", false)]
+ [InlineData("invmem_nullptr2", "std::ptr::null()", "10", false)]
+ [InlineData("invmem_longer", "c_str.as_ptr()", "1000", false)]
+ [InlineData("invmem_toobigptr", "usize::MAX as *const i8", "200", true)]
+ [InlineData("invmem_maxlen", "c_str.as_ptr()", "usize::MAX", true)]
+ public void LogInvalidMemory(string name, string memAddress, string len, bool shouldError)
+ {
+ var e2e = new CustomRustE2ETest(name, @"use msbuild::logging::{{LogWarning}};"
+ , $@"
+let invalid_json = ""{{\""key\"": \""value\"""";
+let c_str = std::ffi::CString::new(invalid_json).unwrap();
+unsafe{{
+LogWarning({memAddress},{len})
+}}
+println!(""{{}}"", ""{{\""properties\"":{{}}}}"");
+TaskResult::Success
+", "");
+
+ var task = new EmptyWasmTask(e2e.GetWasmFilePath());
+ task.Execute();
+
+
+ if (shouldError)
+ {
+ task.Log.HasLoggedErrors.ShouldBeTrue();
+ }
+ else
+ {
+ task.Log.HasLoggedErrors.ShouldBeFalse();
+ }
+ }
+
+ private class OPropTask : WasmTask
+ {
+ [Output]
+ public string? OProp { get; set; }
+
+ public OPropTask(string wasmPath) : base()
+ {
+ WasmFilePath = wasmPath;
+ BuildEngine = new MockEngine();
+ }
+ }
+
+ [Theory]
+ [InlineData("invout_nothing", "")]
+ [InlineData("invout_empty", "{}")]
+ [InlineData("invout_invalidjson", "{{}")]
+ [InlineData("invout_integerprop", @"{\""properties\"":{\""OProp\"":11}}")]
+ [InlineData("invout_dictprop", @"{\""properties\"":{\""OProp\"":{\""a\"":\""b\""}}}")]
+ [InlineData("invout_arrayprop", @"{\""properties\"":{\""OProp\"":[{\""a\"":\""b\""}]}}")] //sus
+ [InlineData("invout_unspecifiedprop", @"{\""properties\"":{\""OProp\"":{}}}")]
+ public void InvalidOutputShouldError(string name, string output)
+ {
+ var e2e = new CustomRustE2ETest(name, "", @$"println!(""{{}}"", ""{output}"");TaskResult::Success", @"
+ let task_info_struct = TaskInfoStruct {
+ name: String::from(""testtest""),
+ properties: vec![
+ Property {
+ name: String::from(""OProp""),
+ output: true,
+ required: false,
+ property_type: PropertyType::String,
+ },
+ ],
+ };
+ task_info(task_info_struct);
+");
+
+ var task = new OPropTask(e2e.GetWasmFilePath());
+ task.Execute();
+ task.Log.HasLoggedErrors.ShouldBeTrue();
+
+ }
+
}
}