diff --git a/src/EStimLibrary/Core/Haptics/HapticSession.cs b/src/EStimLibrary/Core/Haptics/HapticSession.cs index 8bf0163..965de54 100644 --- a/src/EStimLibrary/Core/Haptics/HapticSession.cs +++ b/src/EStimLibrary/Core/Haptics/HapticSession.cs @@ -44,7 +44,7 @@ public class HapticSession /// /// The global contact IDs that are currently wired to one or more leads. /// - public SortedSet WiredContacts => this._leadManager._WiredContacts; + public SortedSet WiredContacts => this._leadManager.WiredContacts; /// /// The global contact IDs that are currently not wired to any leads. /// @@ -60,7 +60,7 @@ public class HapticSession /// /// The global output IDs that are currently wired to one or more leads. /// - public SortedSet WiredOutputs => this._leadManager._WiredOutputs; + public SortedSet WiredOutputs => this._leadManager.WiredOutputs; /// /// The global output IDs that are currently not wired to any leads. /// diff --git a/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs b/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs index e9c8158..3f22c0b 100644 --- a/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs +++ b/src/EStimLibrary/Core/HardwareInterfaces/LeadManager.cs @@ -18,24 +18,30 @@ public class LeadManager : ResourceManager /// The global contact IDs that are currently wired to one or more leads. /// Only valid IDs are added. /// - internal SortedSet _WiredContacts { get; private set; } + public SortedSet WiredContacts => new(this._ContactLeadIdMap.Keys); /// /// The global output IDs that are currently wired to one or more leads. /// Only valid IDs are added. /// - internal SortedSet _WiredOutputs { get; private set; } - + public SortedSet WiredOutputs => new(this._OutputLeadIdMap.Keys); + /// + /// A map of global contact IDs to the set of global lead IDs they are + /// involved in. + /// protected Dictionary> _ContactLeadIdMap; + /// + /// A map of global output IDs to the set of global lead IDs they are + /// involved in. + /// protected Dictionary> _OutputLeadIdMap; //TODO? public Dictionary ContactOutputMap { get; protected set; } + /// + /// Initialize an empty lead manager with no values initially stored. + /// public LeadManager() { - // Initialize wired ID sets. - this._WiredContacts = new(); - this._WiredOutputs = new(); - // Initialize empty dictionaries and a zero total count of contacts. this._ContactLeadIdMap = new(); this._OutputLeadIdMap = new(); @@ -52,9 +58,8 @@ public LeadManager() /// False if invalid or not wired. public bool IsWiredContact(int contactId) { - // Inherent ID validation bc only valid IDs will be added to - // WiredContacts. - return this._WiredContacts.Contains(contactId); + // Inherent ID validation bc only valid IDs will be in WiredContacts. + return this.WiredContacts.Contains(contactId); } /// @@ -66,7 +71,8 @@ public bool IsWiredContact(int contactId) /// False if invalid or not wired. public bool IsWiredOutput(int outputId) { - return this._WiredOutputs.Contains(outputId); + // Inherent ID validation bc only valid IDs will be in WiredOutputs. + return this.WiredOutputs.Contains(outputId); } /// @@ -81,23 +87,25 @@ public bool IsWiredOutput(int outputId) /// True if the lead could be added, False if not. public bool TryAddLead(Lead lead, out int leadId) { - // Get the next available lead ID and try adding the lead to the - // resource pool. - if (!this.TryGetNextAvailableId(out leadId) || - !this.TryAddResource(leadId, lead)) + // First lets check if the lead already has been given an Id + // If the lead has already been added return true and the lead's Id + if (this.Resources.ContainsValue(lead)) { - // Return early if failed. + // This is guaranteed to be the same as in the Resources since + // Leads are compared based on values and Id is a value. + leadId = lead.Id; + return true; + } + if (!(this.TryGetNextAvailableId(out leadId) && + this.TryAddResource(leadId, lead))) + { + // We return early if this fails return false; } - - // Actually set the lead's ID. + // Update the lead Id lead._Id = leadId; - - // Update extra internal structs. For all contacts and outputs: - // a) Mark as wired. - this._WiredContacts.UnionWith(lead.ContactSet); - this._WiredOutputs.UnionWith(lead.OutputSet); - // b) Add the lead ID to their list of leads. + // Update extra internal structs. For all contacts and outputs, add the + // lead ID to their list of leads. // TODO: why need to make a copy to avoid collection mod? no mod tho... // AND only an issue during debug foreach (var contactId in lead.ContactSet.ToList()) @@ -203,10 +211,11 @@ public bool TryRemoveLead(int leadId, out Lead removedLead) // Remove the lead ID from the contact's lead set. var leadSet = this._ContactLeadIdMap[contactId]; leadSet.Remove(leadId); - // Remove the contact from the "wired" set if no leads left on it. + // If the contact is no longer connected to any leads, remove the + // contact entry entirely. if (leadSet.Count == 0) { - this._WiredContacts.Remove(contactId); + this._ContactLeadIdMap.Remove(contactId); } } foreach (var outputId in removedLead.OutputSet) @@ -214,10 +223,11 @@ public bool TryRemoveLead(int leadId, out Lead removedLead) // Remove the lead ID from the output's lead set. var leadSet = this._OutputLeadIdMap[outputId]; leadSet.Remove(leadId); - // Remove the output from the "wired" set if no leads left on it. + // If the output is no longer connected to any leads, remove the + // output entry entirely. if (leadSet.Count == 0) { - this._WiredOutputs.Remove(outputId); + this._OutputLeadIdMap.Remove(outputId); } } @@ -239,12 +249,13 @@ public bool TryRemoveLead(int leadId, out Lead removedLead) /// not. public bool TryRemoveLead(Lead lead, out int leadId, out Lead removedLead) { - // Try to get the ID of the Lead (Leads are records: Equals by value). - // TODO: actually test if this int? and FirstOrDefault strategy works. - // Was having some troubles with it in another class where HasValue - // returned true but only because the default value (stored upon lookup - // failure) was technically a valid value by that check --> TEST TEST!!! - int? id = this.Resources.FirstOrDefault(kv => kv.Value.Equals(lead)).Key; + // Check if this Id is in the resources pool. + int? id = null; + if (this.Resources.ContainsKey(lead.Id)) + { + // Set the Id of the lead to be removed. + id = lead.Id; + } if (id.HasValue) { // Store the ID of the lead to be removed. @@ -268,6 +279,7 @@ public bool TryRemoveLead(Lead lead, out int leadId, out Lead removedLead) // so I/O calls can be handled by another class public static void SaveMapToCSV(string outfile) { + } public static LeadManager LoadMapFromCSV(string infile) diff --git a/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadManagerTests.cs b/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadManagerTests.cs new file mode 100644 index 0000000..7b7c311 --- /dev/null +++ b/tests/EStimLibrary.UnitTests/Core/HardwareInterfaces/LeadManagerTests.cs @@ -0,0 +1,489 @@ +using EStimLibrary.Core; +using EStimLibrary.Core.HardwareInterfaces; + + +namespace EStimLibrary.UnitTests.Core.HardwareInterfaces; + + +/// +/// Tests for the LeadManager class. +/// +public class LeadManagerTests +{ + /// + /// Test the empty constructor. There is nothing to test since all fields + /// that get initialized are protected or internal with no public get + /// methods so they cannot be checked. + /// + [Fact] + public void EmptyConstuctor_ShouldInitEmpty() + { + var leadManager = new LeadManager(); + // No assertions are needed since we are only ensuring the object + // initializes without error. + } + + /// + /// Test the behavior of the TryAddLead method when adding a lead to an + /// empty LeadManager. Verifies that the method returns the correct ID and + /// updates the wiring state of the contacts and outputs. + /// The LeadManager instance to test the + /// TryAddLead method on. + /// The Lead object to add to the LeadManager. + /// The expected ID that should be assigned to the + /// lead after it is added. + /// + [Theory] + [MemberData(nameof(TryAddLeadEmptyData))] + public void TryAddLead_ShouldReturnCorrectIdWhenLeadMangerIsEmpty( + LeadManager leadManager, Lead lead, + int expectedId) + { + + var results = leadManager.TryAddLead(lead, out var id); + // Assert that the operation is successful and returns the expected ID + Assert.True(results); + Assert.Equal(expectedId, id); + // Assert all contacts in the lead are correctly wired in the lead mgr + foreach (var contact in lead.ContactSet) + { + Assert.True(leadManager.IsWiredContact(contact)); + } + // Assert all outputs in the lead are correctly wired in the lead mgr + foreach (var output in lead.OutputSet) + { + Assert.True(leadManager.IsWiredOutput(output)); + } + } + + /// + /// Test parameter data for TryAddLead, following the form: + /// leadManager (Instance to be used), + /// lead (to be added), + /// expectedId + /// Assuming all global IDs and leads are valid in the session. + /// + public static IEnumerable TryAddLeadEmptyData() + { + // Starting with an empty LeadManager + var leadManager = new LeadManager(); + var lead = new Lead(new SortedSet { 2, 3, 4, 8 }, + new SortedSet { 3 }, Constants.CurrentDirection.SINK); + + return new List + { + // Testing adding a lead when the manager is empty + new object[] + { + leadManager, + new Lead(new SortedSet { 1, 2, 3 }, + new SortedSet { 3, 4, 5 }, + Constants.CurrentDirection.SINK), + 0 + }, + // Testing adding a second lead when the mgr now has one thing in it + new object[] + { + leadManager, + lead, + 1 + }, + // Testing adding the same lead again, we expect to get the same id + new object[] + { + leadManager, + lead, + 1 + } + }; + } + + /// + /// Tests the TryAddLead method when some IDs in the LeadManager are freed. + /// Verifies that the correct ID is returned when adding a lead and ensures + /// that the lead is wired correctly. + /// + /// The LeadManager instance to test the + /// TryAddLead method on. + /// The Lead object to add to the LeadManager. + /// The expected ID to be returned when the lead is + /// added to the manager. + [Theory] + [MemberData(nameof(TryAddLeadFirstIdMissingData))] + public void TryAddLead_ShouldReturnCorrectId_WhenSomeIdFreesUp( + LeadManager leadManager, Lead lead, int expectedId) + { + // Add some initial leads to the manager + leadManager.TryAddLead(new Lead(new SortedSet { 1, 2, 3 }, + new SortedSet { 3, 4, 5 }, Constants.CurrentDirection.SINK), + out _); + leadManager.TryAddLead(new Lead(new SortedSet { 3, 4, 5 }, + new SortedSet { 1, 2, 3 }, Constants.CurrentDirection.SINK), + out _); + leadManager.TryAddLead(new Lead(new SortedSet { 97 }, + new SortedSet { 0, 43, 76 }, Constants.CurrentDirection.SINK), + out _); + + // Remove a lead to free up an ID + leadManager.TryRemoveLead(expectedId, out _); + + var results = leadManager.TryAddLead(lead, out var id); + + // Assert the operation is successful and the correct ID is returned + Assert.True(results); + Assert.Equal(expectedId, id); + + // Ensure the lead is wired correctly + foreach (var contact in lead.ContactSet) + { + Assert.True(leadManager.IsWiredContact(contact)); + } + + foreach (var output in lead.OutputSet) + { + Assert.True(leadManager.IsWiredOutput(output)); + } + } + + /// + /// Data for testing TryAddLead when some lead IDs are freed up in the form: + /// leadManager (Instance to be used), + /// lead (to be added), + /// expectedId + /// Assuming all global IDs and leads are valid in the session. + /// + public static IEnumerable TryAddLeadFirstIdMissingData() + { + var leadManager = new LeadManager(); + + return new List + { + new object[] + { + // Testing when the first lead in the manager is freed up + leadManager, + new Lead(new SortedSet { 2, 3, 4 }, + new SortedSet { 3, 2 }, + Constants.CurrentDirection.SINK), + 0 + }, + new object[] + { + // Testing when a lead in the middle of the manager is freed up + leadManager, + new Lead(new SortedSet { 3, 4, 5 }, + new SortedSet { 1, 2, 3 }, + Constants.CurrentDirection.SINK), + 1 + }, + new object[] + { + // Testing when the last lead in the manager is freed up + leadManager, + new Lead(new SortedSet { 97 }, + new SortedSet { 0, 43, 76 }, + Constants.CurrentDirection.SINK), + 2 + }, + }; + } + /// + /// Method to check GetLeadsOfOutput returns the corrected list of leads. + /// + /// The lead manager instance to perform the tests + /// on. + /// The Id of the output we want to search + /// A list of the leads we expect to be returned + /// from the search + [Theory] + [MemberData(nameof(GetLeadsWiredToOutputData))] + public void GetLeadsWiredToOutput_ShouldReturnCorrectLeads_WhenOutputIsWired( + LeadManager leadManager, int outputId, List expectedLeads) + { + var results = leadManager.GetLeadsOfOutput(outputId); + + // Check that we got the right about of leads + Assert.Equal(expectedLeads.Count, results.Count); + // Check the results are exactly the same as the expected results + foreach (var lead in expectedLeads) + { + Assert.Contains(lead, results); + } + } + + /// + /// Data for testing GetLeadsOfOutput: + /// leadManager (Instance to be used), + /// outputId (The Id of the output to be searched for) + /// expectedLeads (The list of leads we expect to be returned) + /// Assuming all global IDs and leads are valid in the session. + /// + public static IEnumerable GetLeadsWiredToOutputData() + { + var leadManager = new LeadManager(); + + // Setting up the LeadManager with some leads + var lead1 = new Lead(new SortedSet { 1, 2, 3 }, + new SortedSet { 4 }, Constants.CurrentDirection.SINK); + var lead2 = new Lead(new SortedSet { 4, 5 }, + new SortedSet { 6 }, Constants.CurrentDirection.SINK); + var lead3 = new Lead(new SortedSet { 7, 8 }, + new SortedSet { 4 }, Constants.CurrentDirection.SINK); + + leadManager.TryAddLead(lead1, out _); + leadManager.TryAddLead(lead2, out _); + leadManager.TryAddLead(lead3, out _); + + return new List + { + // Testing the case that many leads are returned + new object[] + { + leadManager, + 4, + new List {lead1, lead3} + }, + // Testing the case that one lead is returned + new object[] + { + leadManager, + 6, + new List {lead2} + }, + // Testing with an output ID that is not in any of the leads + new object[] + { + leadManager, + 10, + new List() + }, + // Testing with an invalid ID + new object[] + { + leadManager, + -1, + new List() + } + }; + } + + /// + /// A method to test TryRemoveLead when the input lead exists. + /// + /// The lead manager instance to perform the tests + /// on. + /// The Id of the lead to be removed. + /// The lead we expected to be removed and + /// returned. + [Theory] + [MemberData(nameof(RemoveLeadData))] + public void RemoveLead_ShouldReturnTrueAndRemoveLead_WhenLeadExists( + LeadManager leadManager, int leadId, Lead expectedLead) + { + var result = leadManager.TryRemoveLead(leadId, out var removedLead); + + // We expect that the method will always succeed + Assert.True(result); + // We check removed lead vs the expected lead + Assert.Equal(expectedLead, removedLead); + // Assuming at least one output + var outputs = leadManager.GetLeadsOfOutput( + expectedLead.OutputSet.First()); + // Make sure the outputs don't contain the lead we removed + Assert.DoesNotContain(expectedLead, outputs); + } + + /// + /// Data for testing RemoveLead when the input data is valid + /// leadManager (Instance to be used), + /// leadId (The Id of the lead to be removed), + /// expectedLead (The lead we expect to be removed) + /// Assuming all global IDs and leads are valid in the session. + /// + public static IEnumerable RemoveLeadData() + { + var leadManager = new LeadManager(); + + // Setting up the LeadManager with some leads + var lead1 = new Lead(new SortedSet { 1, 2 }, + new SortedSet { 3 }, Constants.CurrentDirection.SINK); + var lead2 = new Lead(new SortedSet { 4, 5 }, + new SortedSet { 6 }, Constants.CurrentDirection.SINK); + leadManager.TryAddLead(lead1, out var id1); + leadManager.TryAddLead(lead2, out var id2); + + return new List + { + // Test to remove the first lead from the manager + new object[] + { + leadManager, + id1, + lead1 + }, + // Test to remove the last lead from the manager + new object[] + { + leadManager, + id2, + lead2 + } + }; + } + + /// + /// Method to check that GetLeadsOfOutput returns the corrected list of + /// leads when the data is invalid. + /// + /// The lead manager instance to perform the tests + /// on. + /// The Id of the output we want to remove (should be + /// invalid). + [Theory] + [MemberData(nameof(RemoveLeadWithInvalidIdData))] + public void RemoveLead_ShouldReturnFalse_WhenLeadDoesNotExist( + LeadManager leadManager, int leadId) + { + var result = leadManager.TryRemoveLead(leadId, out var removedLead); + + // The input should be invalid so we expect the method to fail + Assert.False(result); + // We expect no lead to be removed + Assert.Null(removedLead); + } + + /// + /// Data for testing RemoveLead when the input data is invalid: + /// leadManager (Instance to be used), + /// leadId (The Id of the lead to be removed, will be invalid) + /// + public static IEnumerable RemoveLeadWithInvalidIdData() + { + var leadManager = new LeadManager(); + + // Setting up with some leads + leadManager.TryAddLead(new Lead(new SortedSet { 1, 2 }, + new SortedSet { 3 }, Constants.CurrentDirection.SINK), out _); + + return new List + { + new object[] { leadManager, -1 }, // Negative ID + new object[] { leadManager, 1}, + new object[] { leadManager, 100 } // High invalid ID + }; + } + + /// + /// Testing removing a lead by the value not Id, when the lead to remove is + /// valid. + /// + /// The instance of lead manager the tests will be + /// performed on. + /// The lead that will be removed from the lead + /// manager. + /// The Id of the lead that is expected to be + /// removed. + [Theory] + [MemberData(nameof(RemoveLeadByValueData))] + public void RemoveLeadByValue_ShouldReturnTrueAndRemoveLead_WhenLeadExists( + LeadManager leadManager, Lead leadToRemove, int expectedLeadId) + { + var result = leadManager.TryRemoveLead(leadToRemove, + out var removedLeadId, out var removedLead); + + Assert.True(result); + Assert.Equal(expectedLeadId, removedLeadId); + Assert.Equal(removedLead, leadToRemove); + + // Verify the lead is not returned in any output + var outputs = leadManager.GetLeadsOfOutput( + leadToRemove.OutputSet.First()); + Assert.DoesNotContain(removedLead, outputs); + } + + /// + /// Data for testing RemoveLead by value when the input data is valid + /// leadManager (Instance to be used), + /// leadToRemove + /// expectedLeadId (The Id of the lead we expect to be returned) + /// + public static IEnumerable RemoveLeadByValueData() + { + var leadManager = new LeadManager(); + + // Setting up the LeadManager with some leads + var lead1 = new Lead(new SortedSet { 1, 2 }, + new SortedSet { 3 }, Constants.CurrentDirection.SINK); + var lead2 = new Lead(new SortedSet { 4, 5 }, + new SortedSet { 6 }, Constants.CurrentDirection.SINK); + leadManager.TryAddLead(lead1, out var id1); + leadManager.TryAddLead(lead2, out var id2); + + return new List + { + // Removing the first lead by value + new object[] + { + leadManager, + lead1, + id1 + }, + // Removing the last lead by value + new object[] + { + leadManager, + lead2, + id2 + } + }; + } + + /// + /// Method to check that RemoveLeadByValue returns false when the inputs + /// are invalid. + /// + /// The lead manager instance to perform the tests + /// on. + /// The lead that we will attempt to remove + /// (should be invalid). + [Theory] + [MemberData(nameof(RemoveLeadByValueWithInvalidData))] + public void RemoveLeadByValue_ShouldReturnFalse_WhenLeadDoesNotExist( + LeadManager leadManager, Lead leadToRemove) + { + var result = leadManager.TryRemoveLead(leadToRemove, + out var removedLeadId, out var removedLead); + + Assert.False(result); + Assert.Equal(-1, removedLeadId); + Assert.Null(removedLead); + } + + /// + /// Data for testing RemoveLead by value when the input data is invalid: + /// leadManager (Instance to be used), + /// leadToRemove (The lead to be removed, will be invalid) + /// + public static IEnumerable RemoveLeadByValueWithInvalidData() + { + var leadManager = new LeadManager(); + + // Setting up with some leads + leadManager.TryAddLead(new Lead(new SortedSet { 1, 2 }, + new SortedSet { 3 }, Constants.CurrentDirection.SINK), out _); + + return new List + { + // Non-existing lead + new object[] { leadManager, + new Lead(new SortedSet { 4, 5 }, + new SortedSet { 6 }, + Constants.CurrentDirection.SINK) }, + // Another non-existing lead + new object[] { leadManager, + new Lead(new SortedSet { 10, 11 }, + new SortedSet { 12 }, + Constants.CurrentDirection.SINK) } + }; + } +} \ No newline at end of file