Initial commit: Wonderware / System Platform tools and reference
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Attributes;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Commands;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Protocol;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands
|
||||
{
|
||||
public class GRAccessSurfaceCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(typeof(GalaxyInfoCommand), "galaxy info")]
|
||||
[InlineData(typeof(GalaxyImportScriptLibraryCommand), "galaxy import-script-library")]
|
||||
[InlineData(typeof(ObjectGetCommand), "object get")]
|
||||
[InlineData(typeof(ObjectQueryNameCommand), "object query-name")]
|
||||
[InlineData(typeof(ObjectScriptsCreateCommand), "object scripts create")]
|
||||
[InlineData(typeof(ObjectScriptsSettingsSetCommand), "object scripts settings set")]
|
||||
[InlineData(typeof(TemplateDeriveCommand), "template derive")]
|
||||
[InlineData(typeof(InstanceDeployCommand), "instance deploy")]
|
||||
[InlineData(typeof(ObjectsExportCommand), "objects export")]
|
||||
[InlineData(typeof(ToolsetListCommand), "toolset list")]
|
||||
[InlineData(typeof(ScriptLibraryExportCommand), "script-library export")]
|
||||
[InlineData(typeof(SecurityRolesCommand), "security roles")]
|
||||
[InlineData(typeof(SettingsLocaleGetCommand), "settings locale get")]
|
||||
public void Command_IsRegistered(Type commandType, string expectedName)
|
||||
{
|
||||
var attr = (CommandAttribute)Attribute.GetCustomAttribute(
|
||||
commandType,
|
||||
typeof(CommandAttribute));
|
||||
|
||||
attr.ShouldNotBeNull();
|
||||
attr.Name.ShouldBe(expectedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PipeRequest_StoresStructuredArgs()
|
||||
{
|
||||
var request = PipeRequest.Execute(
|
||||
"objects",
|
||||
"checkout",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["name"] = new[] { "A", "B" },
|
||||
["confirm"] = true
|
||||
});
|
||||
|
||||
request.Args["name"].ShouldBeAssignableTo<string[]>();
|
||||
((string[])request.Args["name"]).ShouldBe(new[] { "A", "B" });
|
||||
request.Args["confirm"].ShouldBe(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfirmedCommand_IncludesConfirmationArgs()
|
||||
{
|
||||
var command = new ObjectCheckoutCommand
|
||||
{
|
||||
GalaxyName = "ZB",
|
||||
ObjectName = "DEV",
|
||||
Confirm = true,
|
||||
ConfirmTarget = "DEV"
|
||||
};
|
||||
|
||||
var args = command.Args();
|
||||
|
||||
args["confirm"].ShouldBe(true);
|
||||
args["confirm-target"].ShouldBe("DEV");
|
||||
args["name"].ShouldBe("DEV");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BulkCommand_PreservesMultipleNames()
|
||||
{
|
||||
var command = new ObjectsCheckoutCommand
|
||||
{
|
||||
GalaxyName = "ZB",
|
||||
Names = new[] { "A", "B" },
|
||||
Confirm = true,
|
||||
ConfirmTarget = "A,B"
|
||||
};
|
||||
|
||||
var names = ((IReadOnlyList<string>)command.Args()["name"]).ToArray();
|
||||
|
||||
names.ShouldBe(new[] { "A", "B" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UdaCommands_DefaultToValidWritableCategory()
|
||||
{
|
||||
new ObjectUdaAddCommand().Args()["category"].ShouldBe("MxCategoryWriteable_USC");
|
||||
new ObjectUdaUpdateCommand().Args()["category"].ShouldBe("MxCategoryWriteable_USC");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TemplateDelete_DefaultsToNonCascadingForceOption()
|
||||
{
|
||||
new TemplateDeleteCommand().Args()["force-option"].ShouldBe("dontForceTemplateDelete");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceDelete_DefaultsToDeleteCleanupForceOption()
|
||||
{
|
||||
new InstanceDeleteCommand().Args()["force-option"].ShouldBe("undeployIfDeployed");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("EForceDeleteTemplateOption", "dontForceTemplateDelete", "dontForceTemplateDelete")]
|
||||
[InlineData("EForceDeleteTemplateOption", "galaxy_dontForceTemplateDelete", "dontForceTemplateDelete")]
|
||||
[InlineData("MxAttributeCategory", "mxcategorywriteable_usc", "MxCategoryWriteable_USC")]
|
||||
public void DispatcherEnumParser_AcceptsCaseInsensitiveNamesAndGalaxyPrefixAliases(
|
||||
string enumType,
|
||||
string value,
|
||||
string expected)
|
||||
{
|
||||
var dispatcher = typeof(GalaxyInfoCommand).Assembly.GetType("ZB.MOM.WW.GRAccess.Cli.GRAccess.GRAccessCommandDispatcher");
|
||||
var enumValue = dispatcher.GetMethod("EnumValue", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(string) }, null)
|
||||
.Invoke(null, new object[] { enumType, value });
|
||||
|
||||
enumValue.ToString().ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("AnalogLimitAlarm", "AnalogExtension")]
|
||||
[InlineData("analoglimitalarm", "AnalogExtension")]
|
||||
[InlineData("HistoryExtension", "HistoryExtension")]
|
||||
public void DispatcherExtensionType_NormalizesLegacyAliases(string value, string expected)
|
||||
{
|
||||
var dispatcher = typeof(GalaxyInfoCommand).Assembly.GetType("ZB.MOM.WW.GRAccess.Cli.GRAccess.GRAccessCommandDispatcher");
|
||||
var extensionType = dispatcher.GetMethod("ExtensionType", BindingFlags.Static | BindingFlags.NonPublic)
|
||||
.Invoke(null, new object[] { value });
|
||||
|
||||
extensionType.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatcherAtomicObjectEdit_UsesExpectedLifecycle()
|
||||
{
|
||||
var source = DispatcherSource();
|
||||
var helper = source.Substring(source.IndexOf("private static string AtomicObjectEdit", StringComparison.Ordinal));
|
||||
|
||||
helper.IndexOf("obj.CheckOut();", StringComparison.Ordinal).ShouldBeLessThan(
|
||||
helper.IndexOf("var summary = mutation(obj);", StringComparison.Ordinal));
|
||||
helper.IndexOf("var summary = mutation(obj);", StringComparison.Ordinal).ShouldBeLessThan(
|
||||
helper.IndexOf("obj.Save();", StringComparison.Ordinal));
|
||||
helper.IndexOf("obj.Save();", StringComparison.Ordinal).ShouldBeLessThan(
|
||||
helper.IndexOf("obj.CheckIn(string.Empty);", StringComparison.Ordinal));
|
||||
helper.ShouldContain("obj.UndoCheckOut();");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatcherSingleObjectMutations_UseAtomicEditHelper()
|
||||
{
|
||||
var source = DispatcherSource();
|
||||
|
||||
source.ShouldContain("return AtomicObjectEdit(obj, editObj =>");
|
||||
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptCreate");
|
||||
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptSet");
|
||||
source.ShouldContain("return AtomicObjectEdit(FindSingleObject(galaxy, Kind(args), Arg(args, \"name\")), obj => ObjectScriptSettingsSet");
|
||||
}
|
||||
|
||||
private static string DispatcherSource()
|
||||
{
|
||||
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||
while (directory != null)
|
||||
{
|
||||
var sourcePath = Path.Combine(
|
||||
directory.FullName,
|
||||
"src",
|
||||
"ZB.MOM.WW.GRAccess.Cli",
|
||||
"GRAccess",
|
||||
"GRAccessCommandDispatcher.cs");
|
||||
|
||||
if (File.Exists(sourcePath))
|
||||
return File.ReadAllText(sourcePath);
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Could not locate GRAccessCommandDispatcher.cs from the test working directory.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Infrastructure;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Commands.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands.Galaxy
|
||||
{
|
||||
public class GalaxyListCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithJsonFlag_WritesJsonArray()
|
||||
{
|
||||
// Arrange
|
||||
using var console = new FakeInMemoryConsole();
|
||||
var command = new GalaxyListCommand
|
||||
{
|
||||
NodeName = "TestNode",
|
||||
Json = true
|
||||
};
|
||||
|
||||
// We can't call ExecuteAsync without a real GRAccess install,
|
||||
// but we can verify the command is constructable and properties bind.
|
||||
command.NodeName.ShouldBe("TestNode");
|
||||
command.Json.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_IsRegisteredInApplication()
|
||||
{
|
||||
// Verify the command type has the correct CliFx attribute
|
||||
var attr = (CliFx.Attributes.CommandAttribute)System.Attribute.GetCustomAttribute(
|
||||
typeof(GalaxyListCommand),
|
||||
typeof(CliFx.Attributes.CommandAttribute));
|
||||
|
||||
attr.ShouldNotBeNull();
|
||||
attr.Name.ShouldBe("galaxy list");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_HasOptionalNodeOption()
|
||||
{
|
||||
var prop = typeof(GalaxyListCommand).GetProperty("NodeName");
|
||||
prop.ShouldNotBeNull();
|
||||
|
||||
var optAttr = (CliFx.Attributes.CommandOptionAttribute)System.Attribute.GetCustomAttribute(
|
||||
prop,
|
||||
typeof(CliFx.Attributes.CommandOptionAttribute));
|
||||
|
||||
optAttr.ShouldNotBeNull();
|
||||
optAttr.Name.ShouldBe("node");
|
||||
optAttr.IsRequired.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.GRAccess.Cli.GRAccess;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Infrastructure;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Commands
|
||||
{
|
||||
public class LlmIntegrationCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("capabilities", false, "")]
|
||||
[InlineData("object snapshot", false, "")]
|
||||
[InlineData("object lineage", false, "")]
|
||||
[InlineData("object children", false, "")]
|
||||
[InlineData("object attribute value get", false, "")]
|
||||
[InlineData("object attribute value set", true, "name")]
|
||||
[InlineData("object scripts list", false, "")]
|
||||
[InlineData("object scripts set", true, "name")]
|
||||
[InlineData("object scripts create", true, "name")]
|
||||
[InlineData("object scripts settings set", true, "name")]
|
||||
[InlineData("area create", true, "template")]
|
||||
[InlineData("engine create", true, "template")]
|
||||
[InlineData("instance assign-area", true, "name")]
|
||||
[InlineData("io assign", true, "name")]
|
||||
[InlineData("batch", false, "")]
|
||||
[InlineData("validate", false, "")]
|
||||
public void CapabilityRegistry_IncludesLlmCommands(string commandName, bool mutates, string confirmTarget)
|
||||
{
|
||||
var command = CommandCapabilityRegistry.Find(commandName);
|
||||
|
||||
command.ShouldNotBeNull();
|
||||
command.Mutates.ShouldBe(mutates);
|
||||
command.ConfirmTarget.ShouldBe(confirmTarget);
|
||||
command.SupportsLlmJson.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CapabilityValidation_RejectsMissingMutationConfirmation()
|
||||
{
|
||||
var result = CommandCapabilityRegistry.Validate(
|
||||
"object attribute value set",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["galaxy"] = "ZB",
|
||||
["name"] = "TestMachine",
|
||||
["attribute"] = "Description",
|
||||
["value"] = "Updated"
|
||||
},
|
||||
requireMutationConfirmation: true);
|
||||
|
||||
result.Valid.ShouldBeFalse();
|
||||
result.Errors.ShouldContain("Missing required confirm=true.");
|
||||
result.Errors.ShouldContain("confirm-target must be 'TestMachine'.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CapabilityValidation_AcceptsConfirmedMutation()
|
||||
{
|
||||
var result = CommandCapabilityRegistry.Validate(
|
||||
"object attribute value set",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["galaxy"] = "ZB",
|
||||
["name"] = "TestMachine",
|
||||
["attribute"] = "Description",
|
||||
["value"] = "Updated",
|
||||
["confirm"] = true,
|
||||
["confirm-target"] = "TestMachine"
|
||||
},
|
||||
requireMutationConfirmation: true);
|
||||
|
||||
result.Valid.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LlmResponse_EnvelopeSerializesSuccessAndUnavailable()
|
||||
{
|
||||
var json = LlmResponse.Ok(
|
||||
"object snapshot",
|
||||
"ZB",
|
||||
"TestMachine",
|
||||
new { name = "TestMachine" },
|
||||
unavailable: new[] { new LlmUnavailableField { Field = "scripts", Reason = "not exposed" } });
|
||||
|
||||
var token = JObject.Parse(json);
|
||||
|
||||
token["success"]?.Value<bool>().ShouldBeTrue();
|
||||
token["command"]?.Value<string>().ShouldBe("object snapshot");
|
||||
token["data"]?["name"]?.Value<string>().ShouldBe("TestMachine");
|
||||
token["unavailable"]?.Count().ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitCommandName_UsesRegistrySubcommand()
|
||||
{
|
||||
var split = CommandCapabilityRegistry.SplitCommandName("object attribute value get");
|
||||
|
||||
split.Command.ShouldBe("object");
|
||||
split.Subcommand.ShouldBe("attribute-value-get");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageSnapshotParser_ExtractsLineageAttributesAndScripts()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".aaPKG");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, @"
|
||||
Lineage: $gMachine -> $TestMachine
|
||||
AttributeValue Name=""Description"" DataType=""MxString"" Value=""Test machine template""
|
||||
ScriptBody Name=""UpdateTestChangingInt.ExecuteText"">
|
||||
me.TestChangingInt = me.TestChangingInt + 1;
|
||||
</ScriptBody>
|
||||
");
|
||||
|
||||
var snapshot = PackageSnapshotParser.Parse(path);
|
||||
|
||||
snapshot.PackageFallbackUsed.ShouldBeTrue();
|
||||
snapshot.Lineage.ShouldBe(new[] { "$gMachine", "$TestMachine" });
|
||||
snapshot.AttributeValues.Single().Name.ShouldBe("Description");
|
||||
snapshot.AttributeValues.Single().Value.ShouldBe("Test machine template");
|
||||
snapshot.ScriptBodies.Single().Name.ShouldBe("UpdateTestChangingInt.ExecuteText");
|
||||
snapshot.ScriptBodies.Single().Body.ShouldContain("TestChangingInt");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageSnapshotParser_ExtractsNestedBinaryScriptExtensionBody()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".aaPKG");
|
||||
try
|
||||
{
|
||||
var innerBytes = Encoding.Unicode.GetBytes(string.Join("\n", new[]
|
||||
{
|
||||
"UpdateTestChangingInt",
|
||||
"UpdateTestChangingInt_ScriptExtension",
|
||||
"Me.TestChangingInt = System.Random().Next(1,1000);",
|
||||
"Periodic"
|
||||
}));
|
||||
|
||||
using (var outer = ZipFile.Open(path, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = outer.CreateEntry("File1.cab");
|
||||
using (var entryStream = entry.Open())
|
||||
using (var inner = new ZipArchive(entryStream, ZipArchiveMode.Create))
|
||||
{
|
||||
var txt = inner.CreateEntry("1055.txt");
|
||||
using (var txtStream = txt.Open())
|
||||
txtStream.Write(innerBytes, 0, innerBytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot = PackageSnapshotParser.Parse(path);
|
||||
var script = snapshot.ScriptBodies.Single(s => s.Name == "UpdateTestChangingInt.ExecuteText");
|
||||
|
||||
script.Body.ShouldBe("Me.TestChangingInt = System.Random().Next(1,1000);");
|
||||
script.Source.ShouldBe("export-package:binary");
|
||||
snapshot.AttributeValues.Single(a => a.Name == "UpdateTestChangingInt.ExecuteText").Value.ShouldBe(script.Body);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.GRAccess.Cli.Session;
|
||||
|
||||
namespace ZB.MOM.WW.GRAccess.Cli.Tests.Session
|
||||
{
|
||||
public class StaComThreadTests : IDisposable
|
||||
{
|
||||
private readonly StaComThread _sut;
|
||||
|
||||
public StaComThreadTests()
|
||||
{
|
||||
_sut = new StaComThread();
|
||||
_sut.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sut.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ExecutesOnStaThread()
|
||||
{
|
||||
var apartmentState = await _sut.RunAsync(() => Thread.CurrentThread.GetApartmentState());
|
||||
|
||||
apartmentState.ShouldBe(ApartmentState.STA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ReturnsResult()
|
||||
{
|
||||
var result = await _sut.RunAsync(() => 42);
|
||||
|
||||
result.ShouldBe(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_PropagatesException()
|
||||
{
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
_sut.RunAsync<int>(() => throw new InvalidOperationException("test error")));
|
||||
|
||||
ex.Message.ShouldBe("test error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_VoidAction_Completes()
|
||||
{
|
||||
var executed = false;
|
||||
|
||||
await _sut.RunAsync(() => { executed = true; });
|
||||
|
||||
executed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_VoidAction_PropagatesException()
|
||||
{
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||
_sut.RunAsync(() => throw new InvalidOperationException("void error")));
|
||||
|
||||
ex.Message.ShouldBe("void error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_MultipleWorkItems_ExecuteInOrder()
|
||||
{
|
||||
var results = new int[3];
|
||||
|
||||
await _sut.RunAsync(() => { results[0] = 1; });
|
||||
await _sut.RunAsync(() => { results[1] = 2; });
|
||||
await _sut.RunAsync(() => { results[2] = 3; });
|
||||
|
||||
results.ShouldBe(new[] { 1, 2, 3 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_AllWorkRunsOnSameThread()
|
||||
{
|
||||
var threadId1 = await _sut.RunAsync(() => Thread.CurrentThread.ManagedThreadId);
|
||||
var threadId2 = await _sut.RunAsync(() => Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
threadId1.ShouldBe(threadId2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CanBeCalledMultipleTimes()
|
||||
{
|
||||
var thread = new StaComThread();
|
||||
thread.Start();
|
||||
thread.Dispose();
|
||||
|
||||
Should.NotThrow(() => thread.Dispose());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunAsync_AfterDispose_ThrowsObjectDisposed()
|
||||
{
|
||||
var thread = new StaComThread();
|
||||
thread.Start();
|
||||
thread.Dispose();
|
||||
|
||||
Should.Throw<ObjectDisposedException>(() => thread.RunAsync(() => { }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>ZB.MOM.WW.GRAccess.Cli.Tests</RootNamespace>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Platforms>x86</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.GRAccess.Cli\ZB.MOM.WW.GRAccess.Cli.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user