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);