diff --git a/pwiz_tools/Skyline/CommandArgUsage.Designer.cs b/pwiz_tools/Skyline/CommandArgUsage.Designer.cs
index 768711227a..77b5814724 100644
--- a/pwiz_tools/Skyline/CommandArgUsage.Designer.cs
+++ b/pwiz_tools/Skyline/CommandArgUsage.Designer.cs
@@ -735,6 +735,24 @@ internal class CommandArgUsage {
}
}
+ ///
+ /// Looks up a localized string similar to Add an ion mobility library to the open document, based on its currently loaded chromatograms..
+ ///
+ internal static string _ionmobility_library_create {
+ get {
+ return ResourceManager.GetString("_ionmobility_library_create", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Name to give the ion mobility library in an –-ionmobility-library-create operation..
+ ///
+ internal static string _ionmobility_library_name {
+ get {
+ return ResourceManager.GetString("_ionmobility_library_name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The name for the iRT calculator created during assay library import. (optional) The default name is the document base name..
///
@@ -1687,6 +1705,15 @@ internal class CommandArgUsage {
}
}
+ ///
+ /// Looks up a localized string similar to Creating an ion mobility library.
+ ///
+ internal static string CommandArgs_GROUP_CREATE_IMSDB_Ion_Mobility_Library {
+ get {
+ return ResourceManager.GetString("CommandArgs_GROUP_CREATE_IMSDB_Ion_Mobility_Library", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Adding decoy peptides.
///
diff --git a/pwiz_tools/Skyline/CommandArgUsage.resx b/pwiz_tools/Skyline/CommandArgUsage.resx
index 2c65e618ab..cd902c7455 100644
--- a/pwiz_tools/Skyline/CommandArgUsage.resx
+++ b/pwiz_tools/Skyline/CommandArgUsage.resx
@@ -123,6 +123,12 @@
Specify a spectral library to be added to the open document.
+
+ Add an ion mobility library to the open document, based on its currently loaded chromatograms.
+
+
+ Name to give the ion mobility library in an –-ionmobility-library-create operation.
+
Runs a file line by line treating each line like a SkylineRunner/Cmd input. Useful for automating the execution of multiple commands. The open Skyline file remains active through all commands.
@@ -387,6 +393,9 @@
Importing results replicates
+
+ Creating an ion mobility library
+
Removing results replicates
diff --git a/pwiz_tools/Skyline/CommandArgs.cs b/pwiz_tools/Skyline/CommandArgs.cs
index 514d0afae8..e59ad0947f 100644
--- a/pwiz_tools/Skyline/CommandArgs.cs
+++ b/pwiz_tools/Skyline/CommandArgs.cs
@@ -34,6 +34,7 @@
using pwiz.Skyline.Model;
using pwiz.Skyline.Model.DocSettings;
using pwiz.Skyline.Model.GroupComparison;
+using pwiz.Skyline.Model.IonMobility;
using pwiz.Skyline.Model.Irt;
using pwiz.Skyline.Model.Results;
using pwiz.Skyline.Model.Results.Scoring;
@@ -215,6 +216,23 @@ public bool Saving
public string SharedFile { get; private set; }
public ShareType SharedFileType { get; private set; }
+ // For creating a .imsdb ion mobility library
+ public static readonly Argument ARG_IMSDB_CREATE = new DocArgument(@"ionmobility-library-create", () => GetPathToFile(IonMobilityDb.EXT),
+ (c, p) => c.ImsDbFile = p.ValueFullPath);
+
+ // For creating a .imsdb ion mobility library
+ public static readonly Argument ARG_IMSDB_NAME = new DocArgument(@"ionmobility-library-name", NAME_VALUE,
+ (c, p) => c.ImsDbName = p.Value);
+
+ private static readonly ArgumentGroup GROUP_CREATE_IMSDB = new ArgumentGroup(() => CommandArgUsage.CommandArgs_GROUP_CREATE_IMSDB_Ion_Mobility_Library, false,
+ ARG_IMSDB_CREATE, ARG_IMSDB_NAME)
+ {
+ Dependencies =
+ {
+ { ARG_IMSDB_NAME, ARG_IMSDB_CREATE },
+ },
+ };
+
public static readonly Argument ARG_IMPORT_FILE = new DocArgument(@"import-file", PATH_TO_FILE,
(c, p) => c.ParseImportFile(p));
public static readonly Argument ARG_IMPORT_REPLICATE_NAME = new DocArgument(@"import-replicate-name", NAME_VALUE,
@@ -337,6 +355,8 @@ private bool ValidateMinimizeResultsArgs()
return true;
}
+ public string ImsDbFile { get; private set; }
+ public string ImsDbName { get; private set; }
public List ReplicateFile { get; private set; }
public string ReplicateName { get; private set; }
@@ -556,6 +576,10 @@ public bool AddDecoys
{
get { return !string.IsNullOrEmpty(AddDecoysType); }
}
+ public bool CreatingIMSDB
+ {
+ get { return !string.IsNullOrEmpty(ImsDbFile); }
+ }
public bool ImportingResults
{
get { return ImportingReplicateFile || ImportingSourceDirectory; }
@@ -1784,6 +1808,7 @@ public static IEnumerable UsageBlocks
GROUP_IMPORT_SEARCH,
GROUP_IMPORT_LIST,
GROUP_ADD_LIBRARY,
+ GROUP_CREATE_IMSDB,
GROUP_DECOYS,
GROUP_REFINEMENT,
GROUP_REFINEMENT_W_RESULTS,
diff --git a/pwiz_tools/Skyline/CommandLine.cs b/pwiz_tools/Skyline/CommandLine.cs
index 4d5432e390..0bf802c365 100644
--- a/pwiz_tools/Skyline/CommandLine.cs
+++ b/pwiz_tools/Skyline/CommandLine.cs
@@ -376,6 +376,11 @@ private bool ProcessDocument(CommandArgs commandArgs)
return false;
}
+ if (commandArgs.ImsDbFile != null && !CreateImsDb(commandArgs))
+ {
+ return false;
+ }
+
if (commandArgs.Saving)
{
var saveFile = commandArgs.SaveFile ?? _skylineFile;
@@ -588,6 +593,40 @@ private bool RefineDocument(CommandArgs commandArgs)
}
}
+ private bool CreateImsDb(CommandArgs commandArgs)
+ {
+ var libName = commandArgs.ImsDbName ?? Path.GetFileNameWithoutExtension(commandArgs.ImsDbFile);
+ var message = string.Format(
+ Resources.CommandLine_CreateImsDb_Creating_ion_mobility_library___0___in___1_____, libName,
+ commandArgs.ImsDbFile);
+ _out.WriteLine(Resources.CommandLine_CreateImsDb_Creating_ion_mobility_library___0___in___1_____, libName, commandArgs.ImsDbFile);
+ try
+ {
+ ModifyDocumentWithLogging(doc => doc.ChangeSettings(doc.Settings.ChangeTransitionIonMobilityFiltering(ionMobilityFiltering =>
+ {
+ var progressMonitor = new CommandProgressMonitor(_out, new ProgressStatus(message));
+ var lib = IonMobilityLibrary.CreateFromResults(
+ doc, null, false, libName, commandArgs.ImsDbFile,
+ progressMonitor);
+
+ return ionMobilityFiltering.ChangeLibrary(lib);
+ })), AuditLogEntry.SettingsLogFunction);
+ return true;
+ }
+ catch (Exception x)
+ {
+ if (!_out.IsErrorReported)
+ {
+ _out.WriteLine(Resources.CommandLine_GeneralException_Error___0_, x.Message);
+ }
+ else
+ {
+ _out.WriteLine(x.Message);
+ }
+ return false;
+ }
+ }
+
private IsotopeLabelType GetLabelTypeHelper(string label)
{
var mods = _doc.Settings.PeptideSettings.Modifications;
diff --git a/pwiz_tools/Skyline/Model/IonMobility/IonMobilityDb.cs b/pwiz_tools/Skyline/Model/IonMobility/IonMobilityDb.cs
index 4a91db9d14..4e89900c6e 100644
--- a/pwiz_tools/Skyline/Model/IonMobility/IonMobilityDb.cs
+++ b/pwiz_tools/Skyline/Model/IonMobility/IonMobilityDb.cs
@@ -435,6 +435,15 @@ public override int GetHashCode()
public static IonMobilityDb CreateIonMobilityDb(string path, string libraryName, bool minimized)
{
+ var directoryName = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
+ {
+ var message =
+ string.Format(
+ Resources.CommandLine_SaveFile_Error__The_file_could_not_be_saved_to__0____Check_that_the_directory_exists_and_is_not_read_only_,
+ path);
+ throw new DirectoryNotFoundException(message);
+ }
const string libAuthority = BiblioSpecLiteLibrary.DEFAULT_AUTHORITY;
const int majorVer = 0; // This will increment when we add data
const int minorVer = DbLibInfo.SCHEMA_VERSION_CURRENT;
diff --git a/pwiz_tools/Skyline/Properties/Resources.Designer.cs b/pwiz_tools/Skyline/Properties/Resources.Designer.cs
index 779f3377c8..9bf40c1d1e 100644
--- a/pwiz_tools/Skyline/Properties/Resources.Designer.cs
+++ b/pwiz_tools/Skyline/Properties/Resources.Designer.cs
@@ -5415,6 +5415,15 @@ public class Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Creating ion mobility library "{0}" in "{1}"....
+ ///
+ public static string CommandLine_CreateImsDb_Creating_ion_mobility_library___0___in___1_____ {
+ get {
+ return ResourceManager.GetString("CommandLine_CreateImsDb_Creating_ion_mobility_library___0___in___1_____", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Error: Importing an assay library to a document without an iRT calculator cannot create {0}, because it exists..
///
@@ -32301,6 +32310,16 @@ public class Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Unable to determine format of delimiter separated value file.
+ ///
+ public static string TextUtil_DeterminDsvSeparator_Unable_to_determine_format_of_delimiter_separated_value_file {
+ get {
+ return ResourceManager.GetString("TextUtil_DeterminDsvSeparator_Unable_to_determine_format_of_delimiter_separated_v" +
+ "alue_file", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to All Files.
///
diff --git a/pwiz_tools/Skyline/Properties/Resources.resx b/pwiz_tools/Skyline/Properties/Resources.resx
index 9f1e598d5e..da7f0f1b94 100644
--- a/pwiz_tools/Skyline/Properties/Resources.resx
+++ b/pwiz_tools/Skyline/Properties/Resources.resx
@@ -11877,4 +11877,10 @@ If you do not have the original file, you may build the library with embedded sp
Submitting an unhandled error report
+
+ Creating ion mobility library "{0}" in "{1}"...
+
+
+ Unable to determine format of delimiter separated value file
+
\ No newline at end of file
diff --git a/pwiz_tools/Skyline/Test/MProphetScoringModelTest.cs b/pwiz_tools/Skyline/Test/MProphetScoringModelTest.cs
index 9fcc2fa278..87db58dc20 100644
--- a/pwiz_tools/Skyline/Test/MProphetScoringModelTest.cs
+++ b/pwiz_tools/Skyline/Test/MProphetScoringModelTest.cs
@@ -32,7 +32,7 @@
namespace pwiz.SkylineTest
{
[TestClass]
- public class MProphetScoringModelTest : AbstractUnitTest
+ public class MProphetScoringModelTest : AbstractUnitTestEx
{
private const string ZIP_FILE = @"Test\MProphetScoringModelTest.zip"; // Not L10N
@@ -377,14 +377,7 @@ public Data(string dataFile)
// Determine separator (comma, space, or tab).
var headerTest = lines[0].Trim();
- var commaCount = headerTest.Split(TextUtil.SEPARATOR_CSV).Length;
- var spaceCount = headerTest.Split(TextUtil.SEPARATOR_SPACE).Length;
- var tabCount = headerTest.Split(TextUtil.SEPARATOR_TSV).Length;
- var maxCount = Math.Max(Math.Max(commaCount, spaceCount), tabCount);
- var separator =
- commaCount == maxCount
- ? TextUtil.SEPARATOR_CSV
- : spaceCount == maxCount ? TextUtil.SEPARATOR_SPACE : TextUtil.SEPARATOR_TSV;
+ var separator = AssertEx.DetermineDsvDelimiter(lines, out var columnCount);
// Find header labels. If all headings are numeric, then no header.
Header = headerTest.ParseDsvFields(separator);
@@ -408,7 +401,7 @@ public Data(string dataFile)
}
// Fill out data matrix.
- Items = new string[lineCount - dataIndex,maxCount];
+ Items = new string[lineCount - dataIndex,columnCount];
for (int i = 0; i < Items.GetLength(0); i++)
{
var items = lines[i + dataIndex].Trim().ParseDsvFields(separator);
diff --git a/pwiz_tools/Skyline/Test/UtilTest.cs b/pwiz_tools/Skyline/Test/UtilTest.cs
index 835127d5d2..1c461064d5 100644
--- a/pwiz_tools/Skyline/Test/UtilTest.cs
+++ b/pwiz_tools/Skyline/Test/UtilTest.cs
@@ -90,8 +90,16 @@ private static void TestDsvFields(char punctuation, char separator)
writer.Write(separator);
writer.WriteDsvField(field, separator);
}
- var fieldsOut = sb.ToString().ParseDsvFields(separator);
- Assert.IsTrue(ArrayUtil.EqualsDeep(fields, fieldsOut), "while parsing:\n"+sb+"\nexpected:\n" + string.Join("\n", fields) + "\n\ngot:\n" + string.Join("\n", fieldsOut));
+
+ var line = sb.ToString();
+ var fieldsOut = line.ParseDsvFields(separator);
+ Assert.IsTrue(ArrayUtil.EqualsDeep(fields, fieldsOut),
+ TextUtil.LineSeparate("while parsing:", line, string.Empty,
+ "expected:", TextUtil.LineSeparate(fields), string.Empty,
+ "got:",TextUtil.LineSeparate(fieldsOut)));
+ var detectedSeparator = AssertEx.DetermineDsvDelimiter(new[] { line }, out var detectedColumnCount);
+ Assert.AreEqual(separator, detectedSeparator);
+ Assert.AreEqual(fields.Length, detectedColumnCount);
}
[TestMethod]
diff --git a/pwiz_tools/Skyline/TestPerf/PerfCommandlineCreateImsDbTest.cs b/pwiz_tools/Skyline/TestPerf/PerfCommandlineCreateImsDbTest.cs
new file mode 100644
index 0000000000..d7ece48cfb
--- /dev/null
+++ b/pwiz_tools/Skyline/TestPerf/PerfCommandlineCreateImsDbTest.cs
@@ -0,0 +1,185 @@
+/*
+ * Original author: Brian Pratt ,
+ * MacCoss Lab, Department of Genome Sciences, UW
+ *
+ * Copyright 2022 University of Washington - Seattle, WA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+using System.IO;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using pwiz.Common.Chemistry;
+using pwiz.Skyline;
+using pwiz.Skyline.Model;
+using pwiz.Skyline.Properties;
+using pwiz.SkylineTestUtil;
+
+namespace TestPerf // Note: tests in the "TestPerf" namespace only run when the global RunPerfTests flag is set
+{
+ ///
+ /// Verify commandline handling for ion mobility library building
+ /// Tests for:
+ /// normal operation
+ /// support for optionally not specifying "--ionmobility-library-name"
+ /// error handling for illegal characters in imsdb filename
+ /// error handling for non-existent subdirectories in imsdb file path
+ /// error handling for specifying "--ionmobility-library-name" without "--ionmobility-library-create"
+ ///
+ [TestClass]
+ public class TestCommandlineCreateImsDbPerf : AbstractUnitTestEx
+ {
+
+ private const string RAW_FILE = "010521_Enamine_U6601911_A1_f100_pos_1_1_1086.d";
+
+ private string GetTestPath(string relativePath)
+ {
+ return TestFilesDirs[0].GetTestPath(relativePath);
+ }
+
+ private string GetImsDbFileName(string testMode)
+ {
+ return $"ImsDbTest{testMode}.imsdb";
+ }
+
+ private string GetImsDbFilePath(string testMode)
+ {
+ return GetTestPath(GetImsDbFileName(testMode));
+ }
+
+ [TestMethod]
+ public void CommandlineCreateImsDbPerfTest()
+ {
+ TestFilesZip = GetPerfTestDataURL(@"PerfCommandlineCreateImsDbTest.zip");
+ TestFilesPersistent = new[] { RAW_FILE }; // list of files that we'd like to unzip alongside parent zipFile, and (re)use in place
+ TestFilesDir = new TestFilesDir(TestContext, TestFilesZip, ".", TestFilesPersistent);
+
+ // Normal use
+ const string normal = @"normal";
+ TestCreateImsdb(normal, GetImsDbFilePath(normal));
+
+ // Support for optionally not specifying "--ionmobility-library-name"
+ TestCreateImsdb(null, GetImsDbFilePath(@"implied-name"));
+
+ // Expect failure because of illegal characters in imsdb filename
+ string imsdbPathBadName = GetImsDbFilePath("bad-name:?");
+ string output = TestCreateImsdb(@"bad-name", imsdbPathBadName, ExpectedResult.error);
+ AssertEx.Contains(output,
+ string.Format(
+ Resources.ValueInvalidPathException_ValueInvalidPathException_The_value___0___is_not_valid_for_the_argument__1__failed_attempting_to_convert_it_to_a_full_file_path_,
+ imsdbPathBadName, CommandArgs.ARG_IMSDB_CREATE.ArgumentText));
+
+ // Expect failure because of nonexistent subdirectories in path
+ const string badPath = @"bad-path";
+ output = TestCreateImsdb(badPath, Path.Combine(badPath, GetImsDbFileName(badPath)), ExpectedResult.error);
+ AssertEx.AreComparableStrings(
+ Resources.CommandLine_SaveFile_Error__The_file_could_not_be_saved_to__0____Check_that_the_directory_exists_and_is_not_read_only_,
+ output);
+
+ // Expect failure because of missing "--ionmobility-library-create"
+ output = TestCreateImsdb(@"bad-args", null, ExpectedResult.warning);
+ AssertEx.Contains(output,
+ string.Format(
+ Resources.CommandArgs_WarnArgRequirment_Warning__Use_of_the_argument__0__requires_the_argument__1_,
+ CommandArgs.ARG_IMSDB_NAME.ArgumentText, CommandArgs.ARG_IMSDB_CREATE.ArgumentText));
+ }
+
+ private enum ExpectedResult { success, warning, error }
+
+ private string TestCreateImsdb(string imsdbName, string imsdbPath, ExpectedResult expectedResult = ExpectedResult.success)
+ {
+ // Clean-up after possible prior runs - also ensures they are not locked
+ string outputPath = GetTestPath("Scripps_IMS_DB.sky");
+ File.Delete(outputPath);
+ string reportFilePath = GetTestPath("Scripps_CCS_report.csv");
+ File.Delete(reportFilePath);
+ if (!string.IsNullOrEmpty(imsdbPath) && File.Exists(imsdbPath))
+ File.Delete(imsdbPath);
+
+ var output = RunCommand(expectedResult != ExpectedResult.error,
+ GetPathArg(CommandArgs.ARG_IN, "Scripps_IMS_Template.sky"),
+ GetArg(CommandArgs.ARG_OUT, outputPath),
+ GetPathArg(CommandArgs.ARG_IMPORT_TRANSITION_LIST, "test_run_1_transition_list.csv"),
+ GetPathArg(CommandArgs.ARG_IMPORT_FILE, RAW_FILE),
+ GetOptionalArg(CommandArgs.ARG_IMSDB_CREATE, imsdbPath),
+ GetOptionalArg(CommandArgs.ARG_IMSDB_NAME, imsdbName),
+ GetArg(CommandArgs.ARG_REPORT_NAME, "Precursor CCS"),
+ GetArg(CommandArgs.ARG_REPORT_FILE, reportFilePath));
+
+ if (expectedResult == ExpectedResult.error)
+ {
+ // These files get created in the case of a warning, even if that
+ // may seem a bit undesirable.
+ Assert.IsFalse(File.Exists(outputPath));
+ Assert.IsFalse(File.Exists(reportFilePath));
+ }
+ else if (expectedResult == ExpectedResult.success)
+ {
+ AssertEx.FileExists(imsdbPath);
+
+ // Compare to expected report - may need to localize the expected copy to match the actual copy
+ AssertEx.AreEquivalentDsvFiles(GetTestPath("ImsDbTest_expected.csv"), reportFilePath, true);
+
+ // Finally, check the persisted document to make sure it loads the IMS library
+ // information that was just added.
+ var doc = ResultsUtil.DeserializeDocument(outputPath);
+
+ AssertEx.IsDocumentState(doc, 0, 1, 53, 53);
+
+ using var docContainer = new ResultsTestDocumentContainer(null, outputPath, true);
+ docContainer.SetDocument(doc, null, true);
+ docContainer.AssertComplete();
+
+ doc = docContainer.Document;
+
+ AssertResult.IsDocumentResultsState(doc, Path.GetFileNameWithoutExtension(RAW_FILE), 53, 53, 0, 53, 0);
+
+ var imFiltering = doc.Settings.TransitionSettings.IonMobilityFiltering;
+ Assert.IsNotNull(imFiltering);
+ Assert.IsTrue(imFiltering.IonMobilityLibrary != null && !imFiltering.IonMobilityLibrary.IsNone);
+
+ foreach (var ppp in doc.MoleculePrecursorPairs)
+ {
+ AssertEx.AreEqual(ExplicitTransitionGroupValues.EMPTY, ppp.NodeGroup.ExplicitValues,
+ "Expected no explicit values to be set, should all be in library");
+ var libKey = ppp.NodeGroup.GetLibKey(doc.Settings, ppp.NodePep);
+ var libEntries = imFiltering.GetIonMobilityInfoFromLibrary(libKey);
+ Assert.IsNotNull(libEntries);
+ Assert.AreEqual(1, libEntries.Count);
+ var libInfo = libEntries.First();
+ AssertEx.AreEqual(eIonMobilityUnits.inverse_K0_Vsec_per_cm2, libInfo.IonMobility.Units);
+ Assert.IsNotNull(libInfo.CollisionalCrossSectionSqA);
+ }
+ }
+
+ return output;
+ }
+
+ private string GetArg(CommandArgs.Argument arg, string value)
+ {
+ return arg.GetArgumentTextWithValue(value);
+ }
+
+ private string GetOptionalArg(CommandArgs.Argument arg, string value)
+ {
+ return string.IsNullOrEmpty(value) ? string.Empty : GetArg(arg, value);
+ }
+
+ private string GetPathArg(CommandArgs.Argument arg, string value)
+ {
+ return GetArg(arg, GetTestPath(value));
+ }
+ }
+}
diff --git a/pwiz_tools/Skyline/TestPerf/TestPerf.csproj b/pwiz_tools/Skyline/TestPerf/TestPerf.csproj
index 40818793a1..7907c73227 100644
--- a/pwiz_tools/Skyline/TestPerf/TestPerf.csproj
+++ b/pwiz_tools/Skyline/TestPerf/TestPerf.csproj
@@ -141,6 +141,7 @@
+
diff --git a/pwiz_tools/Skyline/TestUtil/AbstractUnitTestEx.cs b/pwiz_tools/Skyline/TestUtil/AbstractUnitTestEx.cs
index 2f72c1e60a..4ba46b414a 100644
--- a/pwiz_tools/Skyline/TestUtil/AbstractUnitTestEx.cs
+++ b/pwiz_tools/Skyline/TestUtil/AbstractUnitTestEx.cs
@@ -37,24 +37,48 @@ namespace pwiz.SkylineTestUtil
public class AbstractUnitTestEx : AbstractUnitTest
{
protected static string RunCommand(params string[] inputArgs)
+ {
+ return RunCommand(null, inputArgs);
+ }
+
+ protected static string RunCommand(bool? expectSuccess, params string[] inputArgs)
{
var consoleBuffer = new StringBuilder();
- var consoleOutput = new CommandStatusWriter(new StringWriter(consoleBuffer));
- var exitStatus = CommandLineRunner.RunCommand(inputArgs, consoleOutput, true);
+ var consoleWriter = new CommandStatusWriter(new StringWriter(consoleBuffer));
+
+ var exitStatus = CommandLineRunner.RunCommand(inputArgs, consoleWriter, true);
- var fail = exitStatus == Program.EXIT_CODE_SUCCESS && consoleOutput.IsErrorReported ||
- exitStatus != Program.EXIT_CODE_SUCCESS && !consoleOutput.IsErrorReported;
- if (fail)
+ var consoleOutput = consoleBuffer.ToString();
+ bool errorReported = consoleWriter.IsErrorReported;
+
+ ValidateRunExitStatus(expectSuccess, exitStatus, errorReported, consoleOutput);
+
+ return consoleOutput;
+ }
+
+ private static void ValidateRunExitStatus(bool? expectSuccess, int exitStatus, bool errorReported, string consoleOutput)
+ {
+ string message = null;
+ // Make sure exist status and text error reporting match
+ if (exitStatus == Program.EXIT_CODE_SUCCESS && errorReported ||
+ exitStatus != Program.EXIT_CODE_SUCCESS && !errorReported)
+ {
+ message = string.Format("{0} reported but exit status was {1}.",
+ errorReported ? "Error" : "No error", exitStatus);
+ }
+ else if (expectSuccess.HasValue)
{
- var message =
- TextUtil.LineSeparate(
- string.Format("{0} reported but exit status was {1}.",
- consoleOutput.IsErrorReported ? "Error" : "No error", exitStatus),
- "Output: ", consoleBuffer.ToString());
- Assert.Fail(message);
+ // Make sure expected exit status matches actual
+ if (expectSuccess.Value && exitStatus != Program.EXIT_CODE_SUCCESS)
+ message = string.Format("Expecting successful command-line execution but got {0} exit code.", exitStatus);
+ else if (!expectSuccess.Value && exitStatus == Program.EXIT_CODE_SUCCESS)
+ message = "Expecting command-line error but execution was successful.";
}
- return consoleBuffer.ToString();
+ if (message != null)
+ {
+ Assert.Fail(TextUtil.LineSeparate(message, "Output: ", consoleOutput));
+ }
}
public SrmDocument ConvertToSmallMolecules(SrmDocument doc, ref string docPath, IEnumerable dataPaths,
diff --git a/pwiz_tools/Skyline/TestUtil/AssertEx.cs b/pwiz_tools/Skyline/TestUtil/AssertEx.cs
index cd429d52a3..3480324afd 100644
--- a/pwiz_tools/Skyline/TestUtil/AssertEx.cs
+++ b/pwiz_tools/Skyline/TestUtil/AssertEx.cs
@@ -848,8 +848,8 @@ public static void NoDiff(string target, string actual, string helpMsg=null, Dic
var matchExpected = regexGUID.Match(lineExpected);
var matchActual = regexGUID.Match(lineActual);
if (matchExpected.Success && matchActual.Success
- && Equals(matchExpected.Groups[1].ToString(), matchActual.Groups[1].ToString())
- && Equals(matchExpected.Groups[2].ToString(), matchActual.Groups[2].ToString()))
+ && Equals(matchExpected.Groups[1].ToString(), matchActual.Groups[1].ToString())
+ && Equals(matchExpected.Groups[2].ToString(), matchActual.Groups[2].ToString()))
{
return true;
}
@@ -861,8 +861,8 @@ public static void NoDiff(string target, string actual, string helpMsg=null, Dic
matchExpected = regexTimestamp.Match(lineExpected);
matchActual = regexTimestamp.Match(lineActual);
if (matchExpected.Success && matchActual.Success
- && Equals(matchExpected.Groups[1].ToString(), matchActual.Groups[1].ToString())
- && Equals(matchExpected.Groups[2].ToString(), matchActual.Groups[2].ToString()))
+ && Equals(matchExpected.Groups[1].ToString(), matchActual.Groups[1].ToString())
+ && Equals(matchExpected.Groups[2].ToString(), matchActual.Groups[2].ToString()))
{
return true;
}
@@ -914,6 +914,102 @@ public static void FileEquals(string path1, string path2, Dictionary
+ /// Compare two DSV files, accounting for possible L10N differences
+ ///
+ public static void AreEquivalentDsvFiles(string path1, string path2, bool hasHeaders)
+ {
+ var lines1 = File.ReadAllLines(path1);
+ var lines2 = File.ReadAllLines(path2);
+ AreEqual(lines1.Length, lines2.Length, "Expected same line count");
+ if (lines1.Length == 0)
+ {
+ return;
+ }
+
+ var sep1 = DetermineDsvDelimiter(lines1, out var colCount1);
+ var sep2 = DetermineDsvDelimiter(lines2, out var colCount2);
+ for (var lineNum = 0; lineNum < lines1.Length; lineNum++)
+ {
+ var cols1 = lines1[lineNum].ParseDsvFields(sep1);
+ var cols2 = lines2[lineNum].ParseDsvFields(sep2);
+ AreEqual(cols1.Length, cols2.Length, $"Expected same column count at line {lineNum}");
+ if (hasHeaders && Equals(lineNum, 0) && !Equals(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, @"en"))
+ {
+ continue; // Don't expect localized headers to match
+ }
+ for (var colNum = 0; colNum < cols1.Length; colNum++)
+ {
+ var same = Equals(cols1[colNum], cols2[colNum]);
+
+ if (!same)
+ {
+ // Possibly a decimal value, or even a field like "1.234[M+H]" vs "1,234[M+H]"
+ string Dotted(string val)
+ {
+ return val.Replace(CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator, @"_dot_").
+ Replace(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator, @"_dot_");
+ }
+ same = Equals(Dotted(cols1[colNum]), Dotted(cols2[colNum]));
+ }
+
+ if (!same)
+ {
+ AreEqual(cols1[colNum], cols2[colNum], $"Difference at row {lineNum} column {colNum}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Examine the lines of a DSV file an attempt to determine what kind of delimiter it uses
+ /// N.B. NOT ROBUST ENOUGH FOR GENERAL USE - would likely fail, for example, on data that has
+ /// irregular column counts. But still useful in the test context where we aren't handed random
+ /// data sets from users.
+ ///
+ /// lines of the file
+ /// return value: column count
+ /// the identified delimiter
+ /// thrown when we can't figure it out
+ public static char DetermineDsvDelimiter(string[] lines, out int columnCount)
+ {
+
+ // If a candidate delimiter yields different column counts line to line, it's probably not the right one.
+ // So parse some distance in to see which delimiters give a consistent column count.
+ // NOTE we do see files like that in the wild, but not in our test suite
+ var countsPerLinePerCandidateDelimiter = new Dictionary>
+ {
+ { TextUtil.SEPARATOR_CSV, new List()},
+ { TextUtil.SEPARATOR_SPACE, new List()},
+ { TextUtil.SEPARATOR_TSV, new List()},
+ { TextUtil.SEPARATOR_CSV_INTL, new List()}
+ };
+
+ for (var lineNum = 0; lineNum < Math.Min(100, lines.Length); lineNum++)
+ {
+ foreach (var sep in countsPerLinePerCandidateDelimiter.Keys)
+ {
+ countsPerLinePerCandidateDelimiter[sep].Add((new DsvFileReader(new StringReader(lines[lineNum]), sep)).NumberOfFields);
+ }
+ }
+
+ var likelyCandidates =
+ countsPerLinePerCandidateDelimiter.Where(kvp => kvp.Value.Distinct().Count() == 1).ToArray();
+ if (likelyCandidates.Length > 0)
+ {
+ // The candidate that yields the highest column count wins
+ var maxColumnCount = likelyCandidates.Max(kvp => kvp.Value[0]);
+ if (likelyCandidates.Count(kvp => Equals(maxColumnCount, kvp.Value[0])) == 1)
+ {
+ var delimiter = likelyCandidates.First(kvp => Equals(maxColumnCount, kvp.Value[0])).Key;
+ columnCount = maxColumnCount;
+ return delimiter;
+ }
+ }
+
+ throw new LineColNumberedIoException(Resources.TextUtil_DeterminDsvSeparator_Unable_to_determine_format_of_delimiter_separated_value_file, 1, 1);
+ }
+
public static void FieldsEqual(string target, string actual, int countFields, bool allowForNumericPrecisionDifferences = false)
{
FieldsEqual(target, actual, countFields, null, allowForNumericPrecisionDifferences);