kicad-source/qa/tests/eeschema/test_annotation_refdes_tracker_units.cpp
Seth Hillbrand 15166a9f14 Refactor REFDES_TRACKER
Keep state for reuse in the class.  This allows us to properly pick the
value when fully reannotating

Don't annotate when placing new units.  Just step ahead in the unit
value while keeping the refdes number constant.  This eliminates the
jumping around to unplaced units when placing new multi-unit symbols

Fixes https://gitlab.com/kicad/code/kicad/-/issues/21378
2025-07-25 16:16:09 -07:00

429 lines
15 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include "eeschema_test_utils.h"
#include <refdes_tracker.h>
#include <sch_reference_list.h>
struct REFDES_UNITS_TEST_CASE
{
std::string m_caseName;
std::string m_testRefPrefix;
std::string m_testRefValue;
std::map<int, std::vector<std::tuple<std::string, int>>> m_refNumberMap; // Map of ref number to vector of (value, unit) tuples
std::vector<int> m_requiredUnits;
int m_minValue;
int m_expectedResult;
std::vector<std::string> m_trackerPreloads; // References to preload in tracker
};
class TEST_REFDES_TRACKER_UNITS : public KI_TEST::SCHEMATIC_TEST_FIXTURE
{
protected:
void runTestCase( const REFDES_UNITS_TEST_CASE& testCase );
// Helper method to create test references
SCH_REFERENCE createTestReference( const std::string& aRefPrefix, const std::string& aValue, int aUnit )
{
SCH_SYMBOL dummySymbol;
SCH_SHEET_PATH dummyPath;
SCH_REFERENCE ref( &dummySymbol, dummyPath );
ref.SetRef( aRefPrefix );
ref.SetValue( aValue );
ref.SetUnit( aUnit );
return ref;
}
// Helper method to setup units checker
void setupRefDesTracker( REFDES_TRACKER& tracker )
{
tracker.SetReuseRefDes( false ); // Disable reuse for these tests
tracker.SetUnitsChecker( []( const SCH_REFERENCE& aTestRef,
const std::vector<SCH_REFERENCE>& aExistingRefs,
const std::vector<int>& aRequiredUnits )
{
// Check if all required units are available
for( int unit : aRequiredUnits )
{
for( const auto& ref : aExistingRefs )
{
if( ref.GetUnit() == unit
|| ref.CompareValue( aTestRef ) != 0 )
{
return false; // Conflict found
}
}
}
return true; // All required units are available
} );
}
};
void TEST_REFDES_TRACKER_UNITS::runTestCase( const REFDES_UNITS_TEST_CASE& testCase )
{
REFDES_TRACKER tracker;
// Preload tracker with existing references
for( const std::string& ref : testCase.m_trackerPreloads )
{
tracker.Insert( ref );
}
// Create test reference
SCH_REFERENCE testRef = createTestReference( testCase.m_testRefPrefix, testCase.m_testRefValue, 1 );
// Convert test case data to actual SCH_REFERENCE map
std::map<int, std::vector<SCH_REFERENCE>> refNumberMap;
for( const auto& [refNum, tupleVec] : testCase.m_refNumberMap )
{
std::vector<SCH_REFERENCE> refs;
for( const auto& [value, unit] : tupleVec )
{
refs.push_back( createTestReference( testCase.m_testRefPrefix, value, unit ) );
}
refNumberMap[refNum] = refs;
}
BOOST_TEST_INFO( "Testing case: " + testCase.m_caseName );
setupRefDesTracker( tracker );
// Test GetNextRefDesForUnits logic using the 4-parameter method
int result = tracker.GetNextRefDesForUnits( testRef,
refNumberMap,
testCase.m_requiredUnits,
testCase.m_minValue );
BOOST_CHECK_EQUAL( result, testCase.m_expectedResult );
// Additional verification: check that the result reference is in the tracker
// (unless it was a case where units were available in existing reference)
std::string resultRefDes = testCase.m_testRefPrefix + std::to_string( result );
// Check if this reference number was already in use
bool wasInUse = testCase.m_refNumberMap.find( result ) != testCase.m_refNumberMap.end();
if( !wasInUse && !testCase.m_requiredUnits.empty() )
{
// For completely new references, it should be added to tracker
BOOST_CHECK( tracker.Contains( resultRefDes ) );
}
}
static const std::vector<REFDES_UNITS_TEST_CASE> refdesUnitsTestCases = {
{
"Case 1: Completely unused reference - empty units",
"U", "LM358",
{}, // No currently used references
{}, // Empty required units (need completely unused)
1, // Min value
1, // Expected: U1
{} // No preloaded references
},
{
"Case 2: Completely unused reference - with units",
"U", "LM358",
{}, // No currently used references
{1, 2}, // Need units 1 and 2
1, // Min value
1, // Expected: U1
{} // No preloaded references
},
{
"Case 3: Skip currently in use reference",
"U", "LM358",
{
{ 1, { std::make_tuple("LM358", 1) } } // U1 unit 1 in use
},
{1, 2}, // Need units 1 and 2
1, // Min value
2, // Expected: U2 (U1 conflicts with unit 1)
{}
},
{
"Case 4: Units available in currently used reference",
"U", "LM358",
{
{ 1, { std::make_tuple("LM358", 3),
std::make_tuple("LM358", 4) } } // U1 units 3,4 in use
},
{1, 2}, // Need units 1 and 2 (available)
1, // Min value
1, // Expected: U1 (units 1,2 are free)
{}
},
{
"Case 5: Different value conflict",
"U", "LM358",
{
{ 1, { std::make_tuple("LM741", 1) } } // U1 different value
},
{1}, // Need unit 1
1, // Min value
2, // Expected: U2 (can't share with different value)
{}
},
{
"Case 6: Previously used reference in tracker",
"U", "LM358",
{}, // No currently used references
{1}, // Need unit 1
1, // Min value
2, // Expected: U2 (U1 was previously used)
{"U1"} // U1 preloaded in tracker
},
{
"Case 7: Min value higher than available",
"U", "LM358",
{
{ 5, { std::make_tuple("LM358", 1) } } // U5 unit 1 in use
},
{2}, // Need unit 2
10, // Min value = 10
10, // Expected: U10 (U5 has unit 2 available, but min value is 10)
{}
},
{
"Case 8: Negative units filtered out",
"U", "LM358",
{},
{-1, 1, -5, 2}, // Mix of negative and positive units
1, // Min value
1, // Expected: U1 (only units 1,2 considered)
{}
},
{
"Case 9: Complex scenario with gaps",
"IC", "74HC00",
{
{ 2, { std::make_tuple("74HC00", 1) } }, // IC2 unit 1 used
{ 4, { std::make_tuple("74HC00", 2) } } // IC4 unit 2 used
},
{1, 3}, // Need units 1 and 3
1, // Min value
3, // Expected: IC3 (IC1 unused, IC2 conflicts unit 1, IC3 available)
{"IC1"} // IC1 previously used
}
};
BOOST_FIXTURE_TEST_SUITE( RefDesTrackerUnits, TEST_REFDES_TRACKER_UNITS )
BOOST_AUTO_TEST_CASE( GetNextRefDesForUnits_BasicCases )
{
for( const REFDES_UNITS_TEST_CASE& testCase : refdesUnitsTestCases )
{
runTestCase( testCase );
}
}
BOOST_AUTO_TEST_CASE( GetNextRefDesForUnits_EdgeCases )
{
REFDES_TRACKER tracker;
// Test empty required units vector - should find completely unused reference
SCH_REFERENCE testRef = createTestReference( "R", "1k", 1 );
std::map<int, std::vector<SCH_REFERENCE>> emptyMap;
std::vector<int> emptyUnits;
setupRefDesTracker( tracker );
int result = tracker.GetNextRefDesForUnits( testRef, emptyMap, emptyUnits, 1 );
BOOST_CHECK_EQUAL( result, 1 );
BOOST_CHECK( tracker.Contains( "R1" ) );
// Test with some references already in tracker
tracker.Insert( "R3" );
result = tracker.GetNextRefDesForUnits( testRef, emptyMap, emptyUnits, 1 );
BOOST_CHECK_EQUAL( result, 2 ); // Should skip R1 (already inserted above) and get R2
// Test with negative units (should be filtered out)
std::vector<int> mixedUnits = {-1, 1, -5, 2};
SCH_REFERENCE testRef2 = createTestReference( "C", "100nF", 1 );
result = tracker.GetNextRefDesForUnits( testRef2, emptyMap, mixedUnits, 1 );
BOOST_CHECK_EQUAL( result, 1 );
}
BOOST_AUTO_TEST_CASE( GetNextRefDesForUnits_UsagePattern )
{
REFDES_TRACKER tracker;
// Demonstrate actual usage pattern for GetNextRefDesForUnits with our test helper
SCH_REFERENCE testRef = createTestReference( "U", "LM358", 1 );
// Create map of currently used references
std::map<int, std::vector<SCH_REFERENCE>> refNumberMap;
refNumberMap[1] = { createTestReference("U", "LM358", 1),
createTestReference("U", "LM358", 2) }; // U1 has units 1,2 used
refNumberMap[3] = { createTestReference("U", "LM358", 1) }; // U3 has unit 1 used
// Specify required units for new symbol
std::vector<int> requiredUnits = {1, 2};
setupRefDesTracker( tracker );
// Get next available reference number
int nextRefNum = tracker.GetNextRefDesForUnits( testRef, refNumberMap, requiredUnits, 1 );
// Should return 2 (U2) since U1 conflicts (units 1,2 already used) and U2 is available
BOOST_CHECK_EQUAL( nextRefNum, 2 );
BOOST_CHECK( tracker.Contains( "U2" ) );
// Test case where units are available in existing reference
std::vector<int> requiredUnits2 = {3, 4}; // These should be available in U1
refNumberMap[3] = { createTestReference("U", "LM358", 1) }; // U1 only has unit 1 and 2 used
nextRefNum = tracker.GetNextRefDesForUnits( testRef, refNumberMap, requiredUnits2, 1 );
BOOST_CHECK_EQUAL( nextRefNum, 1 ); // U1 should work since units 3,4 are available
// Test different value conflict
SCH_REFERENCE differentValueRef = createTestReference( "U", "LM741", 1 );
refNumberMap[4] = { createTestReference("U", "LM358", 1) }; // U4 has different value
nextRefNum = tracker.GetNextRefDesForUnits( differentValueRef, refNumberMap, {1}, 4 );
BOOST_CHECK_EQUAL( nextRefNum, 5 ); // Should skip U4 due to value conflict
}
BOOST_AUTO_TEST_CASE( GetNextRefDesForUnits_ThreadSafety )
{
REFDES_TRACKER tracker( true ); // Enable thread safety
// Test that GetNextRefDesForUnits works with thread safety enabled
SCH_REFERENCE testRef = createTestReference( "U", "LM358", 1 );
std::map<int, std::vector<SCH_REFERENCE>> emptyMap;
std::vector<int> requiredUnits = {1, 2};
setupRefDesTracker( tracker );
int result = tracker.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( result, 1 );
BOOST_CHECK( tracker.Contains( "U1" ) );
// Test multiple calls work correctly with thread safety
result = tracker.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( result, 2 );
BOOST_CHECK( tracker.Contains( "U2" ) );
// Test with conflicts and thread safety
std::map<int, std::vector<SCH_REFERENCE>> conflictMap;
conflictMap[3] = { createTestReference("U", "LM358", 1) }; // U3 unit 1 in use
result = tracker.GetNextRefDesForUnits( testRef, conflictMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( result, 4 ); // Should skip U1,U2 (in tracker) and U3 (conflicted)
}
BOOST_AUTO_TEST_CASE( GetNextRefDesForUnits_Integration )
{
REFDES_TRACKER tracker;
// Test that GetNextRefDesForUnits properly integrates with existing Insert/Contains
tracker.Insert( "U1" );
tracker.Insert( "U3" );
BOOST_CHECK( tracker.Contains( "U1" ) );
BOOST_CHECK( tracker.Contains( "U3" ) );
BOOST_CHECK( !tracker.Contains( "U2" ) );
// Test GetNextRefDesForUnits with preloaded tracker
SCH_REFERENCE testRef = createTestReference( "U", "LM358", 1 );
std::map<int, std::vector<SCH_REFERENCE>> emptyMap;
std::vector<int> requiredUnits = {1, 2};
setupRefDesTracker( tracker );
// Should get U2 since U1 is already in tracker (preloaded) and U3 is also preloaded
int next = tracker.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( next, 2 );
BOOST_CHECK( tracker.Contains( "U2" ) );
// Test with higher minimum value
next = tracker.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 5 );
BOOST_CHECK_EQUAL( next, 5 );
BOOST_CHECK( tracker.Contains( "U5" ) );
// Test integration with serialization
std::string serialized = tracker.Serialize();
REFDES_TRACKER tracker2;
BOOST_CHECK( tracker2.Deserialize( serialized ) );
// Verify deserialized tracker has the same state
BOOST_CHECK( tracker2.Contains( "U1" ) );
BOOST_CHECK( tracker2.Contains( "U2" ) );
BOOST_CHECK( tracker2.Contains( "U3" ) );
BOOST_CHECK( tracker2.Contains( "U5" ) );
setupRefDesTracker( tracker2 );
// GetNextRefDesForUnits should work with deserialized tracker
next = tracker2.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( next, 4 ); // Should get U4 (first available after U1,U2,U3,U5)
}
BOOST_AUTO_TEST_CASE( Serialization_WithTrackedReferences )
{
REFDES_TRACKER tracker;
// Add some references using both Insert and GetNextRefDesForUnits
tracker.Insert( "R1" );
tracker.Insert( "R3" );
setupRefDesTracker( tracker );
// Use GetNextRefDesForUnits to get next reference
SCH_REFERENCE testRef = createTestReference( "R", "1k", 1 );
std::map<int, std::vector<SCH_REFERENCE>> emptyMap;
std::vector<int> requiredUnits = {1};
int next = tracker.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( next, 2 ); // Should get R2
// Test with different prefix
SCH_REFERENCE capacitorRef = createTestReference( "C", "100nF", 1 );
next = tracker.GetNextRefDesForUnits( capacitorRef, emptyMap, requiredUnits, 5 );
BOOST_CHECK_EQUAL( next, 5 ); // Should get C5
// Test serialization
std::string serialized = tracker.Serialize();
BOOST_CHECK( !serialized.empty() );
// Test deserialization
REFDES_TRACKER tracker2;
BOOST_CHECK( tracker2.Deserialize( serialized ) );
BOOST_CHECK( tracker2.Contains( "R1" ) );
BOOST_CHECK( tracker2.Contains( "R2" ) );
BOOST_CHECK( tracker2.Contains( "R3" ) );
BOOST_CHECK( tracker2.Contains( "C5" ) );
setupRefDesTracker( tracker2 );
// Test GetNextRefDesForUnits with deserialized tracker
next = tracker2.GetNextRefDesForUnits( testRef, emptyMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( next, 4 ); // Next reference should be R4
// Test with unit conflicts after deserialization
std::map<int, std::vector<SCH_REFERENCE>> conflictMap;
conflictMap[4] = { createTestReference("R", "2k", 1) }; // R4 different value
next = tracker2.GetNextRefDesForUnits( testRef, conflictMap, requiredUnits, 1 );
BOOST_CHECK_EQUAL( next, 5 ); // Should skip R4 due to value conflict
}
BOOST_AUTO_TEST_SUITE_END()