diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Parser/ConfigParser.cs b/tests/MSDIAL5/MsdialCoreTestApp/Parser/ConfigParser.cs index aafc86052..e10460976 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Parser/ConfigParser.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Parser/ConfigParser.cs @@ -164,6 +164,29 @@ private static MethodKeyOutcome Count(string text, Action assign) { return MethodKeyOutcome.UnusableValue; } + /// + /// Read an on/off parameter, accepting "true" and "false" in any letter case. + /// + /// + /// Several arms used to assign only one of the two values - + /// case "keep original precursor isotopes": if (valueLower == "false") param.X = false; return true; + /// - so the other value was reported as applied and silently ignored. That was harmless + /// only while the constructor default happened to equal the value the arm could not + /// assign; KeepOriginalPrecursorIsotopes defaults to false, so "True" never took effect. + /// Anything other than true or false ("yes", "1", "on") is refused rather than guessed + /// at, because ParameterBase writes these keys with bool.ToString() and any other spelling + /// came from somewhere that did not know the format. + /// + private static MethodKeyOutcome TrueOrFalse(string text, Action assign) { + if (string.Equals(text, "true", StringComparison.OrdinalIgnoreCase)) { + return Assign(true, assign); + } + if (string.Equals(text, "false", StringComparison.OrdinalIgnoreCase)) { + return Assign(false, assign); + } + return MethodKeyOutcome.UnusableValue; + } + /// /// Try the second reader only when the first did not recognise the key. /// @@ -401,14 +424,6 @@ private static T ReadFirst(string filepath, T fallback, Func assign) { - var valueLower = text.ToLower(); - if (valueLower == "true" || valueLower == "false") { - return Assign(bool.Parse(valueLower), assign); - } - return MethodKeyOutcome.UnusableValue; - } - public static List ReadMspAnnotatorSettings(string filepath, ParameterBase param) { var settingsFilePath = ReadMspAnnotatorSettingsFilePath(filepath); if (settingsFilePath.IsEmptyOrNull()) { @@ -941,11 +956,9 @@ public static MethodKeyOutcome ReadGcmsSpecificParameter(MsdialGcmsParameter par case "retention index alignment tolerance": return Number(valueLower, v => param.RetentionIndexAlignmentTolerance = (float)v); case "replace quant mass by user defined value": - if (valueLower == "true") - param.IsReplaceQuantmassByUserDefinedValue = true; return true; + return TrueOrFalse(valueLower, v => param.IsReplaceQuantmassByUserDefinedValue = v); case "is quant mass based on base peak mz": - if (valueLower == "true") - param.IsRepresentativeQuantMassBasedOnBasePeakMz = true; return true; + return TrueOrFalse(valueLower, v => param.IsRepresentativeQuantMassBasedOnBasePeakMz = v); default: return false; } } @@ -960,9 +973,7 @@ public static MethodKeyOutcome ReadLcImMsSpecificParameter(MsdialLcImMsParameter case "drift time end": return Number(value, v => param.DriftTimeEnd = (float)v); case "accumulated rt ragne": return Number(value, v => param.AccumulatedRtRange = (float)v); case "accumulate ms2 spectra": - if (value == "true") - param.IsAccumulateMS2Spectra = true; - return true; + return TrueOrFalse(value, v => param.IsAccumulateMS2Spectra = v); case "drift time alignment tolerance": return Number(value, v => param.DriftTimeAlignmentTolerance = (float)v); case "drift time alignment factor": return Number(value, v => param.DriftTimeAlignmentFactor = (float)v); case "ion mobility type": @@ -1017,7 +1028,7 @@ private static MethodKeyOutcome ReadMoleculerNetworkingParameter(MolecularSpectr if (value == "Bonanza" || value == "ModDot" || value == "Cosine" || value == "All") param.MsmsSimilarityCalc = (MsmsSimilarityCalc)Enum.Parse(typeof(MsmsSimilarityCalc), value, true); return true; case "mnisexportioncorrelation": - if (valueLower == "true" || valueLower == "false") param.MnIsExportIonCorrelation = bool.Parse(valueLower); return true; + return TrueOrFalse(valueLower, v => param.MnIsExportIonCorrelation = v); default: return false; } } @@ -1118,13 +1129,9 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m // Private version case "is private version of tada": - if (valueLower == "true") - param.IsLabPrivateVersionTada = true; - return true; + return TrueOrFalse(valueLower, v => param.IsLabPrivateVersionTada = v); case "is private version": - if (valueLower == "true") - param.IsLabPrivate = true; - return true; + return TrueOrFalse(valueLower, v => param.IsLabPrivate = v); //Data correction case "retention time begin": return Number(valueLower, v => param.RetentionTimeBegin = (float)v); @@ -1173,8 +1180,8 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "amplitude cut off": return Number(valueLower, v => param.ChromDecBaseParam.AmplitudeCutoff = (float)v); case "relative amplitude cut off": return Number(valueLower, v => param.ChromDecBaseParam.RelativeAmplitudeCutoff = (float)v); case "keep isotope range": return Number(valueLower, v => param.KeptIsotopeRange = (float)v); - case "exclude after precursor": if (valueLower == "false") param.RemoveAfterPrecursor = false; return true; - case "keep original precursor isotopes": if (valueLower == "false") param.KeepOriginalPrecursorIsotopes = false; return true; + case "exclude after precursor": return TrueOrFalse(valueLower, v => param.RemoveAfterPrecursor = v); + case "keep original precursor isotopes": return TrueOrFalse(valueLower, v => param.KeepOriginalPrecursorIsotopes = v); case "target ce": return Number(valueLower, v => param.TargetCE = v); //Identification @@ -1209,15 +1216,14 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "total score cutoff for msp-based annotation": return Number(valueLower, v => param.MspSearchParam.TotalScoreCutoff = (float)v); case "ms1 tolerance for msp-based annotation": return Number(valueLower, v => param.MspSearchParam.Ms1Tolerance = (float)v); case "ms2 tolerance for msp-based annotation": return Number(valueLower, v => param.MspSearchParam.Ms2Tolerance = (float)v); - case "use retention information for msp-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.MspSearchParam.IsUseTimeForAnnotationScoring = bool.Parse(valueLower); return true; - case "use retention information for msp-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.MspSearchParam.IsUseTimeForAnnotationFiltering = bool.Parse(valueLower); return true; - case "use ccs for msp-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.MspSearchParam.IsUseCcsForAnnotationScoring = bool.Parse(valueLower); return true; - case "use ccs for msp-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.MspSearchParam.IsUseCcsForAnnotationFiltering = bool.Parse(valueLower); return true; - case "only report top hit for msp-based annotation": if (valueLower == "true" || valueLower == "false") param.OnlyReportTopHitInMspSearch = bool.Parse(valueLower); return true; + case "use retention information for msp-based annotation scoring": return TrueOrFalse(valueLower, v => param.MspSearchParam.IsUseTimeForAnnotationScoring = v); + case "use retention information for msp-based annotation filtering": return TrueOrFalse(valueLower, v => param.MspSearchParam.IsUseTimeForAnnotationFiltering = v); + case "use ccs for msp-based annotation scoring": return TrueOrFalse(valueLower, v => param.MspSearchParam.IsUseCcsForAnnotationScoring = v); + case "use ccs for msp-based annotation filtering": return TrueOrFalse(valueLower, v => param.MspSearchParam.IsUseCcsForAnnotationFiltering = v); + case "only report top hit for msp-based annotation": return TrueOrFalse(valueLower, v => param.OnlyReportTopHitInMspSearch = v); case "execute annotation process only for alignment file": case "execute annotation process only for alignment file for msp-based annotation": - if (valueLower == "true" || valueLower == "false") param.IsIdentificationOnlyPerformedForAlignmentFile = bool.Parse(valueLower); - return true; + return TrueOrFalse(valueLower, v => param.IsIdentificationOnlyPerformedForAlignmentFile = v); //Identification case "rt tolerance for lbm-based annotation": return Number(valueLower, v => param.LbmSearchParam.RtTolerance = (float)v); @@ -1238,11 +1244,11 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "total score cutoff for lbm-based annotation": return Number(valueLower, v => param.LbmSearchParam.TotalScoreCutoff = (float)v); case "ms1 tolerance for lbm-based annotation": return Number(valueLower, v => param.LbmSearchParam.Ms1Tolerance = (float)v); case "ms2 tolerance for lbm-based annotation": return Number(valueLower, v => param.LbmSearchParam.Ms2Tolerance = (float)v); - case "use retention information for lbm-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.LbmSearchParam.IsUseTimeForAnnotationScoring = bool.Parse(valueLower); return true; - case "use retention information for lbm-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.LbmSearchParam.IsUseTimeForAnnotationFiltering = bool.Parse(valueLower); return true; - case "use ccs for lbm-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.LbmSearchParam.IsUseCcsForAnnotationScoring = bool.Parse(valueLower); return true; - case "use ccs for lbm-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.LbmSearchParam.IsUseCcsForAnnotationFiltering = bool.Parse(valueLower); return true; - case "execute annotation process only for alignment file for lbm-based annotation": if (valueLower == "true" || valueLower == "false") param.IsIdentificationOnlyPerformedForAlignmentFile = bool.Parse(valueLower); return true; + case "use retention information for lbm-based annotation scoring": return TrueOrFalse(valueLower, v => param.LbmSearchParam.IsUseTimeForAnnotationScoring = v); + case "use retention information for lbm-based annotation filtering": return TrueOrFalse(valueLower, v => param.LbmSearchParam.IsUseTimeForAnnotationFiltering = v); + case "use ccs for lbm-based annotation scoring": return TrueOrFalse(valueLower, v => param.LbmSearchParam.IsUseCcsForAnnotationScoring = v); + case "use ccs for lbm-based annotation filtering": return TrueOrFalse(valueLower, v => param.LbmSearchParam.IsUseCcsForAnnotationFiltering = v); + case "execute annotation process only for alignment file for lbm-based annotation": return TrueOrFalse(valueLower, v => param.IsIdentificationOnlyPerformedForAlignmentFile = v); //Post identification @@ -1251,11 +1257,11 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "ccs tolerance for text-based annotation": return Number(valueLower, v => param.TextDbSearchParam.CcsTolerance = (float)v); case "total score cutoff for text-based annotation": return Number(valueLower, v => param.TextDbSearchParam.TotalScoreCutoff = (float)v); case "accurate ms1 tolerance for text-based annotation": return Number(valueLower, v => param.TextDbSearchParam.Ms1Tolerance = (float)v); - case "use retention information for text-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.TextDbSearchParam.IsUseTimeForAnnotationScoring = bool.Parse(valueLower); return true; - case "use retention information for text-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.TextDbSearchParam.IsUseTimeForAnnotationFiltering = bool.Parse(valueLower); return true; - case "use ccs for text-based annotation scoring": if (valueLower == "true" || valueLower == "false") param.TextDbSearchParam.IsUseCcsForAnnotationScoring = bool.Parse(valueLower); return true; - case "use ccs for text-based annotation filtering": if (valueLower == "true" || valueLower == "false") param.TextDbSearchParam.IsUseCcsForAnnotationFiltering = bool.Parse(valueLower); return true; - case "only report top hit for text-based annotation": if (valueLower == "true" || valueLower == "false") param.OnlyReportTopHitInTextDBSearch = bool.Parse(valueLower); return true; + case "use retention information for text-based annotation scoring": return TrueOrFalse(valueLower, v => param.TextDbSearchParam.IsUseTimeForAnnotationScoring = v); + case "use retention information for text-based annotation filtering": return TrueOrFalse(valueLower, v => param.TextDbSearchParam.IsUseTimeForAnnotationFiltering = v); + case "use ccs for text-based annotation scoring": return TrueOrFalse(valueLower, v => param.TextDbSearchParam.IsUseCcsForAnnotationScoring = v); + case "use ccs for text-based annotation filtering": return TrueOrFalse(valueLower, v => param.TextDbSearchParam.IsUseCcsForAnnotationFiltering = v); + case "only report top hit for text-based annotation": return TrueOrFalse(valueLower, v => param.OnlyReportTopHitInTextDBSearch = v); //Alignment parameters setting case "alignment reference file id": return Count(valueLower, v => param.AlignmentReferenceFileID = v); @@ -1265,9 +1271,9 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "spectrum similarity factor for alignment": return Number(valueLower, v => param.SpectrumSimilarityAlignmentFactor = (float)v); case "ms1 tolerance for alignment": return Number(valueLower, v => param.Ms1AlignmentTolerance = (float)v); case "ms1 factor for alignment": return Number(valueLower, v => param.Ms1AlignmentFactor = (float)v); - case "force insert peaks in gap filling": if (valueLower == "true" || valueLower == "false") param.IsForceInsertForGapFilling = bool.Parse(valueLower); return true; - case "together with alignment": if (valueLower == "true" || valueLower == "false") param.TogetherWithAlignment = bool.Parse(valueLower); return true; - case "execute automatic rt correction for alignment": if (valueLower == "true" || valueLower == "false") param.AlignmentBaseParam.AutomaticRtCorrection.Execute = bool.Parse(valueLower); return true; + case "force insert peaks in gap filling": return TrueOrFalse(valueLower, v => param.IsForceInsertForGapFilling = v); + case "together with alignment": return TrueOrFalse(valueLower, v => param.TogetherWithAlignment = v); + case "execute automatic rt correction for alignment": return TrueOrFalse(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.Execute = v); case "automatic rt correction reference file id": return Count(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.ReferenceFileId = v); case "automatic rt correction rt bin width": return Number(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.RtBinWidth = (float)v); case "automatic rt correction match rt tolerance": return Number(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.MatchRtTolerance = (float)v); @@ -1281,12 +1287,12 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "automatic rt correction minimum ideal slope": return Number(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.MinimumIdealSlope = (float)v); case "automatic rt correction outlier mad threshold": return Number(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.OutlierMadThreshold = (float)v); case "automatic rt correction reference centrality weight": return Number(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.ReferenceCentralityWeight = (float)v); - case "automatic rt correction interpolate blanks by analytical order": if (valueLower == "true" || valueLower == "false") param.AlignmentBaseParam.AutomaticRtCorrection.InterpolateBlankByAnalyticalOrder = bool.Parse(valueLower); return true; + case "automatic rt correction interpolate blanks by analytical order": return TrueOrFalse(valueLower, v => param.AlignmentBaseParam.AutomaticRtCorrection.InterpolateBlankByAnalyticalOrder = v); //Filtering case "peak count filter": return Number(valueLower, v => param.PeakCountFilter = (float)v); case "n percent detected in one group": return Number(valueLower, v => param.NPercentDetectedInOneGroup = (float)v); - case "remove feature based on peak height fold-change": if (valueLower == "true" || valueLower == "false") param.IsRemoveFeatureBasedOnBlankPeakHeightFoldChange = bool.Parse(valueLower); return true; + case "remove feature based on peak height fold-change": return TrueOrFalse(valueLower, v => param.IsRemoveFeatureBasedOnBlankPeakHeightFoldChange = v); case "blank filtering": if (valueLower.ToLower() == "samplemaxoverblankave") param.BlankFiltering = (BlankFiltering)Enum.Parse(typeof(BlankFiltering), valueLower, true); @@ -1297,14 +1303,14 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m param.FoldChangeForBlankFiltering = (float)v; }); case "sample average / blank average": return Number(valueLower, v => param.SampleAverageOverBlankAverage = (float)v); - case "keep reference matched metabolites": if (valueLower == "true" || valueLower == "false") param.IsKeepRefMatchedMetaboliteFeatures = bool.Parse(valueLower); return true; - case "keep suggested metabolites": if (valueLower == "true" || valueLower == "false") param.IsKeepSuggestedMetaboliteFeatures = bool.Parse(valueLower); return true; - case "keep removable features and assigned tag for checking": if (valueLower == "true" || valueLower == "false") param.IsKeepRemovableFeaturesAndAssignedTagForChecking = bool.Parse(valueLower); return true; - case "replace true zero values with 1/2 of minimum peak height over all samples": if (valueLower == "true" || valueLower == "false") param.IsReplaceTrueZeroValuesWithHalfOfMinimumPeakHeightOverAllSamples = bool.Parse(valueLower); return true; + case "keep reference matched metabolites": return TrueOrFalse(valueLower, v => param.IsKeepRefMatchedMetaboliteFeatures = v); + case "keep suggested metabolites": return TrueOrFalse(valueLower, v => param.IsKeepSuggestedMetaboliteFeatures = v); + case "keep removable features and assigned tag for checking": return TrueOrFalse(valueLower, v => param.IsKeepRemovableFeaturesAndAssignedTagForChecking = v); + case "replace true zero values with 1/2 of minimum peak height over all samples": return TrueOrFalse(valueLower, v => param.IsReplaceTrueZeroValuesWithHalfOfMinimumPeakHeightOverAllSamples = v); //Retentiontime correction - case "execute rt correction": if (valueLower == "true" || valueLower == "false") param.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.ExcuteRtCorrection = bool.Parse(valueLower); return true; - case "rt correction with smoothing for rt diff": if (valueLower == "true" || valueLower == "false") param.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.doSmoothing = bool.Parse(valueLower); return true; + case "execute rt correction": return TrueOrFalse(valueLower, v => param.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.ExcuteRtCorrection = v); + case "rt correction with smoothing for rt diff": return TrueOrFalse(valueLower, v => param.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.doSmoothing = v); case "user setting intercept": return Number(valueLower, v => param.RetentionTimeCorrectionCommon.RetentionTimeCorrectionParam.UserSettingIntercept = (float)v); case "rt diff calc method": if (valueLower == "sampleminussampleaverage" || valueLower == "sampleminusreference") @@ -1333,8 +1339,8 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m }); //Isotope tracking setting - case "tracking isotope label": if (valueLower == "true" || valueLower == "false") param.TrackingIsotopeLabels = bool.Parse(valueLower); return true; - case "set fully labeled reference file": if (valueLower == "true" || valueLower == "false") param.SetFullyLabeledReferenceFile = bool.Parse(valueLower); return true; + case "tracking isotope label": return TrueOrFalse(valueLower, v => param.TrackingIsotopeLabels = v); + case "set fully labeled reference file": return TrueOrFalse(valueLower, v => param.SetFullyLabeledReferenceFile = v); case "non labeled reference id": return Count(valueLower, v => param.NonLabeledReferenceID = v); case "fully labeled reference id": return Count(valueLower, v => param.FullyLabeledReferenceID = v); // ParameterBase writes "Number of threads" into every exported method file, @@ -1352,10 +1358,7 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m //CorrDec settings case "corrdec execute": - if (valueLower.ToLower() == "false") { - param.CorrDecParam.CanExcute = false; - } - return true; + return TrueOrFalse(valueLower, v => param.CorrDecParam.CanExcute = v); case "corrdec ms2 tolerance": return Number(valueLower, v => param.CorrDecParam.MS2Tolerance = (float)v); case "corrdec minimum ms2 peak height": @@ -1375,7 +1378,7 @@ public static MethodKeyOutcome ReadCommonParameter(ParameterBase param, string m case "corrdec minimum ms2 relative intensity": return Number(valueLower, v => param.CorrDecParam.MinMS2RelativeIntensity = (float)v); case "corrdec remove peaks larger than precursor": - if (valueLower == "true" || valueLower == "false") param.CorrDecParam.CorrDecRemoveAfterPrecursor = bool.Parse(valueLower); return true; + return TrueOrFalse(valueLower, v => param.CorrDecParam.CorrDecRemoveAfterPrecursor = v); default: return false; } } diff --git a/tests/MSDIAL5/MsdialCoreTestAppTests/Parser/ConfigParserTests.cs b/tests/MSDIAL5/MsdialCoreTestAppTests/Parser/ConfigParserTests.cs index 21748a228..b75f9ffb8 100644 --- a/tests/MSDIAL5/MsdialCoreTestAppTests/Parser/ConfigParserTests.cs +++ b/tests/MSDIAL5/MsdialCoreTestAppTests/Parser/ConfigParserTests.cs @@ -1,6 +1,8 @@ using CompMs.App.MsdialConsole.Parser; using CompMs.Common.DataObj.Result; using CompMs.Common.Enum; +using CompMs.MsdialGcMsApi.Parameter; +using CompMs.MsdialLcImMsApi.Parameter; using CompMs.MsdialLcmsApi.Parameter; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; @@ -123,6 +125,86 @@ public void ReadCommonParameter_ReadsTheTwoKeysABadRenameHadMadeUnreachable() Assert.IsTrue(parameter.IsReplaceTrueZeroValuesWithHalfOfMinimumPeakHeightOverAllSamples); } + [DataTestMethod] + [DataRow("keep original precursor isotopes")] + [DataRow("exclude after precursor")] + [DataRow("corrdec execute")] + [DataRow("is private version")] + [DataRow("is private version of tada")] + public void ReadCommonParameter_ReadsAnOnOffKeyInBothDirections(string key) + { + // THE REGRESSION. These arms assigned only one of the two values and returned true for + // either, so the other value was reported as applied and silently ignored. + // KeepOriginalPrecursorIsotopes defaults to false, so "Keep original precursor isotopes: + // True" never took effect while the key record said it had. + var parameter = new MsdialLcmsParameter(); + Func current = key switch + { + "keep original precursor isotopes" => () => parameter.KeepOriginalPrecursorIsotopes, + "exclude after precursor" => () => parameter.RemoveAfterPrecursor, + "corrdec execute" => () => parameter.CorrDecParam.CanExcute, + "is private version" => () => parameter.IsLabPrivate, + "is private version of tada" => () => parameter.IsLabPrivateVersionTada, + _ => throw new ArgumentOutOfRangeException(nameof(key)), + }; + + AssertReadsBothDirections(value => ConfigParser.ReadCommonParameter(parameter, key, value), current); + } + + [DataTestMethod] + [DataRow("replace quant mass by user defined value")] + [DataRow("is quant mass based on base peak mz")] + public void ReadGcmsSpecificParameter_ReadsAnOnOffKeyInBothDirections(string key) + { + var parameter = new MsdialGcmsParameter(); + Func current = key switch + { + "replace quant mass by user defined value" => () => parameter.IsReplaceQuantmassByUserDefinedValue, + "is quant mass based on base peak mz" => () => parameter.IsRepresentativeQuantMassBasedOnBasePeakMz, + _ => throw new ArgumentOutOfRangeException(nameof(key)), + }; + + AssertReadsBothDirections(value => ConfigParser.ReadGcmsSpecificParameter(parameter, key, value), current); + } + + [TestMethod] + public void ReadLcImMsSpecificParameter_ReadsAccumulateMs2SpectraInBothDirections() + { + var parameter = new MsdialLcImMsParameter(); + + AssertReadsBothDirections( + value => ConfigParser.ReadLcImMsSpecificParameter(parameter, "accumulate ms2 spectra", value), + () => parameter.IsAccumulateMS2Spectra); + } + + [TestMethod] + public void ReadCommonParameter_ReportsAnUnreadableOnOffValueOnAKeyThatWasAlreadyTwoWay() + { + // These arms already assigned both values, but still answered true for "yes", so the + // key record said a value was applied that the run never saw. + var parameter = new MsdialLcmsParameter(); + + AssertReadsBothDirections( + value => ConfigParser.ReadCommonParameter(parameter, "together with alignment", value), + () => parameter.TogetherWithAlignment); + } + + private static void AssertReadsBothDirections(Func read, Func current) + { + Assert.IsTrue(read("True").IsApplied); + Assert.IsTrue(current(), "True takes effect"); + + Assert.IsTrue(read("False").IsApplied); + Assert.IsFalse(current(), "False takes effect"); + + Assert.IsTrue(read("TRUE").IsApplied); + Assert.IsTrue(current(), "the letter case does not matter"); + + var unusable = read("yes"); + Assert.IsTrue(unusable.IsUnusableValue, "a value that is neither true nor false is reported, not guessed at"); + Assert.IsTrue(current(), "the refused value left the previous one alone"); + } + [TestMethod] public void ReadCommonParameter_SetsCcsFilteringForLbmAnnotationOnTheLbmParameter() {