feat(cli): instance import-overrides --file (T16)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.CommandLine.Parsing;
|
using System.CommandLine.Parsing;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ public static class InstanceCommands
|
|||||||
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
command.Add(BuildSetBindings(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildSetBindings(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
command.Add(BuildSetOverrides(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildSetOverrides(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
|
command.Add(BuildImportOverrides(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
command.Add(BuildAlarmOverride(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildAlarmOverride(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
command.Add(BuildNativeAlarmSourceOverride(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildNativeAlarmSourceOverride(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
command.Add(BuildSetArea(urlOption, formatOption, usernameOption, passwordOption));
|
command.Add(BuildSetArea(urlOption, formatOption, usernameOption, passwordOption));
|
||||||
@@ -296,6 +298,46 @@ public static class InstanceCommands
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Command BuildImportOverrides(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||||
|
{
|
||||||
|
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
|
||||||
|
var fileOption = new Option<string>("--file") { Description = "Path to the override CSV file (columns: AttributeName,Value,ElementType)", Required = true };
|
||||||
|
|
||||||
|
var cmd = new Command("import-overrides") { Description = "Apply attribute overrides from a CSV file" };
|
||||||
|
cmd.Add(idOption);
|
||||||
|
cmd.Add(fileOption);
|
||||||
|
cmd.SetAction(async (ParseResult result) =>
|
||||||
|
{
|
||||||
|
var id = result.GetValue(idOption);
|
||||||
|
var filePath = result.GetValue(fileOption)!;
|
||||||
|
|
||||||
|
string csvText;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
csvText = File.ReadAllText(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OutputFormatter.WriteError($"Cannot read file '{filePath}': {ex.Message}", "FILE_READ_ERROR");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseResult = OverrideCsvParser.Parse(csvText);
|
||||||
|
if (parseResult.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var error in parseResult.Errors)
|
||||||
|
OutputFormatter.WriteError(error, "INVALID_CSV");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrides = parseResult.Rows.ToDictionary(r => r.AttributeName, r => r.Value);
|
||||||
|
return await CommandHelpers.ExecuteCommandAsync(
|
||||||
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||||
|
new SetInstanceOverridesCommand(id, overrides));
|
||||||
|
});
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
private static Command BuildAlarmOverride(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
private static Command BuildAlarmOverride(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||||
{
|
{
|
||||||
var group = new Command("alarm-override") { Description = "Manage per-instance alarm overrides" };
|
var group = new Command("alarm-override") { Description = "Manage per-instance alarm overrides" };
|
||||||
|
|||||||
@@ -539,6 +539,30 @@ scadabridge --url <url> instance set-overrides --id <int> --overrides <json>
|
|||||||
| `--id` | yes | Instance ID |
|
| `--id` | yes | Instance ID |
|
||||||
| `--overrides` | yes | JSON object of attribute name to value (e.g. `{"Speed": "100", "Mode": null}`); null clears an override |
|
| `--overrides` | yes | JSON object of attribute name to value (e.g. `{"Speed": "100", "Mode": null}`); null clears an override |
|
||||||
|
|
||||||
|
#### `instance import-overrides`
|
||||||
|
|
||||||
|
Apply attribute value overrides from a CSV file. The CSV must have a header row (`AttributeName,Value,ElementType`; the `ElementType` column is optional). An empty `Value` cell clears the override for that attribute. If the file contains any parse errors, all errors are printed and no overrides are applied.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scadabridge --url <url> instance import-overrides --id <int> --file <path.csv>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Required | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `--id` | yes | Instance ID |
|
||||||
|
| `--file` | yes | Path to the override CSV file |
|
||||||
|
|
||||||
|
CSV format example:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
AttributeName,Value,ElementType
|
||||||
|
Speed,100,
|
||||||
|
Mode,,
|
||||||
|
Label,"Hello, World",
|
||||||
|
```
|
||||||
|
|
||||||
|
(`Value` empty = clear the override for that attribute.)
|
||||||
|
|
||||||
#### `instance alarm-override set`
|
#### `instance alarm-override set`
|
||||||
|
|
||||||
Set (upsert) an alarm override on an instance.
|
Set (upsert) an alarm override on an instance.
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the CSV parsing behaviour that backs <c>instance import-overrides</c>:
|
||||||
|
/// parse errors block the apply step; valid CSV produces the expected
|
||||||
|
/// <c>attributeName → value</c> dictionary that <see cref="SetInstanceOverridesCommand"/>
|
||||||
|
/// consumes.
|
||||||
|
/// </summary>
|
||||||
|
public class ImportOverridesTests
|
||||||
|
{
|
||||||
|
// ── error path ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CsvWithMissingHeader_ProducesErrors()
|
||||||
|
{
|
||||||
|
var result = OverrideCsvParser.Parse("Speed,100");
|
||||||
|
|
||||||
|
Assert.NotEmpty(result.Errors);
|
||||||
|
Assert.Empty(result.Rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CsvWithBlankAttributeName_ProducesError()
|
||||||
|
{
|
||||||
|
const string csv = "AttributeName,Value\n,100";
|
||||||
|
var result = OverrideCsvParser.Parse(csv);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result.Errors);
|
||||||
|
// The bad row is excluded; no rows flow through.
|
||||||
|
Assert.Empty(result.Rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CsvWithWrongColumnCount_ProducesError()
|
||||||
|
{
|
||||||
|
const string csv = "AttributeName,Value\nSpeed,100,extra";
|
||||||
|
var result = OverrideCsvParser.Parse(csv);
|
||||||
|
|
||||||
|
Assert.NotEmpty(result.Errors);
|
||||||
|
Assert.Empty(result.Rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── happy path ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidCsv_ProducesCorrectOverridesDictionary()
|
||||||
|
{
|
||||||
|
const string csv = """
|
||||||
|
AttributeName,Value
|
||||||
|
Speed,100
|
||||||
|
Mode,
|
||||||
|
Label,Hello
|
||||||
|
""";
|
||||||
|
|
||||||
|
var parseResult = OverrideCsvParser.Parse(csv);
|
||||||
|
|
||||||
|
Assert.Empty(parseResult.Errors);
|
||||||
|
Assert.Equal(3, parseResult.Rows.Count);
|
||||||
|
|
||||||
|
// Build the same dictionary that BuildImportOverrides produces.
|
||||||
|
var overrides = parseResult.Rows.ToDictionary(r => r.AttributeName, r => r.Value);
|
||||||
|
|
||||||
|
Assert.Equal("100", overrides["Speed"]);
|
||||||
|
Assert.Null(overrides["Mode"]); // empty Value → null → clear the override
|
||||||
|
Assert.Equal("Hello", overrides["Label"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidCsvWithElementTypeColumn_OverridesDictIgnoresElementType()
|
||||||
|
{
|
||||||
|
const string csv = """
|
||||||
|
AttributeName,Value,ElementType
|
||||||
|
Speed,100,Float
|
||||||
|
Mode,,
|
||||||
|
""";
|
||||||
|
|
||||||
|
var parseResult = OverrideCsvParser.Parse(csv);
|
||||||
|
|
||||||
|
Assert.Empty(parseResult.Errors);
|
||||||
|
Assert.Equal(2, parseResult.Rows.Count);
|
||||||
|
|
||||||
|
// ElementType is carried on the row but SetInstanceOverridesCommand only
|
||||||
|
// takes attributeName → value; confirm the dict shape matches.
|
||||||
|
var overrides = parseResult.Rows.ToDictionary(r => r.AttributeName, r => r.Value);
|
||||||
|
|
||||||
|
Assert.Equal("100", overrides["Speed"]);
|
||||||
|
Assert.Null(overrides["Mode"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user