fix for issue 17429 - DRC exclusion instability

- backported from master
- fix for issue-17429 - DRC unnconnected items exclusion instability
  - new serialization format for unconnected items (MainID and AuxID
    last)
- RunOnUnconnectedItems sorts RN_NET edges for stability
- ResolveDRCExclusions now matches unconnected items by not matching on
      MainID and AuxID
This commit is contained in:
Columba Livia 2025-06-29 14:25:47 -07:00
parent 9ea4b899a2
commit 4a99dc1a99
10 changed files with 9534 additions and 9 deletions

View File

@ -345,8 +345,49 @@ void BOARD::RecordDRCExclusions()
m_designSettings->m_DrcExclusionComments[ serialized ] = marker->GetComment();
}
}
if( m_project )
{
if( PROJECT_FILE* projectFile = &m_project->GetProjectFile() )
{
if( BOARD_DESIGN_SETTINGS* prjSettings = projectFile->m_BoardSettings )
{
prjSettings->m_DrcExclusions = m_designSettings->m_DrcExclusions;
prjSettings->m_DrcExclusionComments = m_designSettings->m_DrcExclusionComments;
}
}
}
}
std::set<wxString>::iterator FindByFirstNFields( std::set<wxString>& strSet,
const wxString& searchStr, char delimiter, int n )
{
wxString searchPrefix = searchStr;
// Extract first n fields from the search string
int delimiterCount = 0;
size_t pos = 0;
while( pos < searchPrefix.length() && delimiterCount < n )
{
if( searchPrefix[pos] == delimiter )
delimiterCount++;
pos++;
}
if( delimiterCount == n )
searchPrefix = searchPrefix.Left( pos - 1 ); // Exclude the nth delimiter
for( auto it = strSet.begin(); it != strSet.end(); ++it )
{
if( it->StartsWith( searchPrefix + delimiter ) || *it == searchPrefix )
{
return it;
}
}
return strSet.end();
}
std::vector<PCB_MARKER*> BOARD::ResolveDRCExclusions( bool aCreateMarkers )
{
@ -358,16 +399,39 @@ std::vector<PCB_MARKER*> BOARD::ResolveDRCExclusions( bool aCreateMarkers )
for( PCB_MARKER* marker : GetBoard()->Markers() )
{
std::set<wxString>::iterator it;
wxString serialized = marker->SerializeToString();
std::set<wxString>::iterator it = exclusions.find( serialized );
wxString matchedExclusion;
if( !serialized.Contains( "unconnected_items" ) )
{
it = exclusions.find( serialized );
if( it != exclusions.end() )
{
matchedExclusion = *it;
}
}
else
{
const int numberOfFieldsExcludingIds = 3;
const char delimiter = '|';
it = FindByFirstNFields( exclusions, serialized, delimiter,
numberOfFieldsExcludingIds );
if( it != exclusions.end() )
{
matchedExclusion = *it;
}
else
{
}
}
if( it != exclusions.end() )
{
marker->SetExcluded( true, comments[ serialized ] );
marker->SetExcluded( true, comments[matchedExclusion] );
// Exclusion still valid; store back to BOARD_DESIGN_SETTINGS
m_designSettings->m_DrcExclusions.insert( serialized );
m_designSettings->m_DrcExclusionComments[ serialized ] = comments[ serialized ];
m_designSettings->m_DrcExclusions.insert( matchedExclusion );
m_designSettings->m_DrcExclusionComments[matchedExclusion] = comments[matchedExclusion];
exclusions.erase( it );
}

View File

@ -86,6 +86,42 @@ public:
return m_weight < aOther.m_weight;
}
/**
* Comparison operator for std::stable_sort.
*
* @param aOther the other edge to compare.
* @return true if this edge should come before aOther in the sorted order.
*
* Comparison order:
* 1. Compare source nodes by position (x, then y)
* 2. Then compare by weight
* 3. Then by visibility
* 4. If everything is equal, return false for stable ordering
*/
bool StableSortCompare( const CN_EDGE& aOther ) const
{
const VECTOR2I& thisPos = GetSourcePos();
const VECTOR2I& otherPos = aOther.GetSourcePos();
// First compare by source node position
if( thisPos.x != otherPos.x )
return thisPos.x < otherPos.x;
if( thisPos.y != otherPos.y )
return thisPos.y < otherPos.y;
// Then compare by weight
if( m_weight != aOther.m_weight )
return m_weight < aOther.m_weight;
// Then by visibility
if( m_visible != aOther.m_visible )
return m_visible && !aOther.m_visible;
// If everything is equal, return false for stable ordering
return false;
}
std::shared_ptr<const CN_ANCHOR> GetSourceNode() const { return m_source; }
std::shared_ptr<const CN_ANCHOR> GetTargetNode() const { return m_target; }

View File

@ -1184,6 +1184,7 @@ void DIALOG_DRC::ExcludeMarker()
return;
RC_TREE_NODE* node = RC_TREE_MODEL::ToNode( m_markerDataView->GetCurrentItem() );
PCB_MARKER* marker = dynamic_cast<PCB_MARKER*>( node->m_RcItem->GetParent() );
if( marker && marker->GetSeverity() != RPT_SEVERITY_EXCLUSION )

View File

@ -23,6 +23,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "connectivity/connectivity_data.h"
#include <bitmaps.h>
#include <base_units.h>
#include <eda_draw_frame.h>
@ -107,6 +108,17 @@ wxString PCB_MARKER::SerializeToString() const
m_rcItem->GetMainItemID().AsString(),
LayerName( m_layer ) );
}
else if( m_rcItem->GetErrorCode() == DRCE_UNCONNECTED_ITEMS )
{
PCB_LAYER_ID layer = m_layer;
if( m_layer == UNDEFINED_LAYER )
layer = F_Cu;
return wxString::Format( wxT( "%s|%d|%d|%s|%d|%s|%s" ), m_rcItem->GetSettingsKey(), m_Pos.x,
m_Pos.y, LayerName( layer ), GetMarkerType(),
m_rcItem->GetMainItemID().AsString(),
m_rcItem->GetAuxItemID().AsString() );
}
else if( m_rcItem->GetErrorCode() == DRCE_STARVED_THERMAL )
{
return wxString::Format( wxT( "%s|%d|%d|%s|%s|%s" ),
@ -171,6 +183,18 @@ PCB_MARKER* PCB_MARKER::DeserializeFromString( const wxString& data )
drcItem->SetItems( KIID( props[3] ) );
markerLayer = getMarkerLayer( props[4] );
}
else if( drcItem->GetErrorCode() == DRCE_UNCONNECTED_ITEMS )
{
// Pre-9.0.3 versions didn't have KIIDs as last two properties to allow sorting stability
if( props.size() < 6 )
{
drcItem->SetItems( KIID( props[3] ), KIID( props[4] ) );
}
else
{
drcItem->SetItems( KIID( props[5] ), KIID( props[6] ) );
}
}
else if( drcItem->GetErrorCode() == DRCE_STARVED_THERMAL )
{
drcItem->SetItems( KIID( props[3] ), KIID( props[4] ) );
@ -204,9 +228,7 @@ void PCB_MARKER::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector<MSG_PANEL_
case RPT_SEVERITY_IGNORE:
aList.emplace_back( _( "Severity" ), _( "Ignore" ) );
break;
case RPT_SEVERITY_WARNING:
aList.emplace_back( _( "Severity" ), _( "Warning" ) );
break;
case RPT_SEVERITY_WARNING: aList.emplace_back( _( "Severity" ), _( "Warning" ) ); break;
case RPT_SEVERITY_ERROR:
aList.emplace_back( _( "Severity" ), _( "Error" ) );
break;

View File

@ -32,6 +32,7 @@
#ifndef RATSNEST_DATA_H
#define RATSNEST_DATA_H
#include <algorithm>
#include <core/typeinfo.h>
#include <math/box2.h>
@ -93,8 +94,28 @@ public:
unsigned int GetNodeCount() const { return m_nodes.size(); }
const std::vector<CN_EDGE>& GetEdges() const { return m_rnEdges; }
std::vector<CN_EDGE>& GetEdges() { return m_rnEdges; }
const std::vector<CN_EDGE>& GetEdges() const
{
// Use a const_cast to allow sorting in the const method
// This is safe because we're not changing the logical content of the vector,
// just the ordering of elements
std::stable_sort( const_cast<std::vector<CN_EDGE>&>( m_rnEdges ).begin(),
const_cast<std::vector<CN_EDGE>&>( m_rnEdges ).end(),
[]( const CN_EDGE& a, const CN_EDGE& b )
{
return a.StableSortCompare( b );
} );
return m_rnEdges;
}
std::vector<CN_EDGE>& GetEdges()
{
std::stable_sort( m_rnEdges.begin(), m_rnEdges.end(),
[]( const CN_EDGE& a, const CN_EDGE& b )
{
return a.StableSortCompare( b );
} );
return m_rnEdges;
}
bool NearestBicoloredPair( RN_NET* aOtherNet, VECTOR2I& aPos1, VECTOR2I& aPos2 ) const;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,960 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": false,
"text_position": 0,
"units_format": 1
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.0,
"height": 1.8,
"width": 1.2
},
"silk_line_width": 0.12,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.15,
"silk_text_upright": false,
"zones": {
"45_degree_only": false,
"min_clearance": 0.508
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
},
{
"gap": 0.127,
"via_gap": 0.127,
"width": 0.154
}
],
"drc_exclusions": [
[
"unconnected_items|115460000|155800000|F.Cu|4|221e4293-1dad-4f91-a869-d39634ca7647|74f57098-b83f-40bf-9a16-2adad8dcb50b",
""
],
[
"unconnected_items|115460000|159000000|F.Cu|4|e5b239f2-afe1-4783-8a0b-bf234fea22df|113054e2-9827-4c9f-a8ee-9dead8ac7000",
""
],
[
"unconnected_items|115460000|161400000|F.Cu|4|cff4aed3-271d-45c0-a3c4-046e16b7f260|d384076d-8217-4237-8a13-86ece3667b99",
""
],
[
"unconnected_items|115460000|165400000|F.Cu|4|74da9126-9d2f-4c1d-b8a1-021e27bcafbe|519972f5-567a-42f1-a038-c4960d508559",
""
],
[
"unconnected_items|115460000|166200000|F.Cu|4|3f4da512-b026-405d-a045-87c506a3ef63|87b98e0c-3614-42af-8982-59c036259cc8",
""
],
[
"unconnected_items|115460000|168600000|F.Cu|4|742ed10c-8c85-48f3-b92b-a749b92a3c8c|c7b6b524-0fb3-41c8-9654-229fe7bd19e7",
""
],
[
"unconnected_items|118540000|154600000|F.Cu|4|69b8910c-2ead-48f4-9358-4be4a4a0663e|539d8a1e-5e53-4252-9a33-1eb2c9717a4b",
""
],
[
"unconnected_items|118540000|155000000|F.Cu|4|d3546ca6-3035-4598-a37d-c76932d318fd|3eab64eb-a1f0-434d-968b-cd0f04f08b5b",
""
],
[
"unconnected_items|118540000|158200000|F.Cu|4|e3fa1053-01eb-44fc-ac99-4eb5213cc1c6|ef60360a-a7b4-4448-90c3-e2532f9767b4",
""
],
[
"unconnected_items|118540000|167800000|F.Cu|4|18383a8f-1c88-4048-a569-5b451205ef1b|c717c398-f57e-451d-8ffd-3863c703056a",
""
]
],
"meta": {
"filename": "board_design_settings.json",
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "error",
"hole_clearance": "error",
"hole_near_hole": "error",
"hole_to_hole": "error",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "error",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"allow_blind_buried_vias": true,
"allow_microvias": true,
"max_error": 0.005,
"min_clearance": 0.127,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.254,
"min_groove_width": 0.0,
"min_hole_clearance": 0.127,
"min_hole_to_hole": 0.254,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.254,
"min_track_width": 0.15,
"min_via_annular_width": 0.1016,
"min_via_diameter": 0.254,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0,
0.127,
0.154,
0.254,
0.4,
0.5
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
},
{
"diameter": 0.3483,
"drill": 0.2467
},
{
"diameter": 0.3556,
"drill": 0.254
},
{
"diameter": 0.4572,
"drill": 0.254
},
{
"diameter": 0.5,
"drill": 0.375
}
],
"zones_allow_external_fillets": false,
"zones_use_no_outline": true
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [
{
"activeLayer": -2,
"flipBoard": false,
"layers": [
4,
8,
10,
12,
14,
16,
18,
20,
22,
24,
25,
26,
28,
30,
32,
34,
36,
38,
40,
42,
44,
46,
48,
50,
52,
54,
56,
58,
60,
62
],
"name": "In.1 Cu - AGND",
"renderLayers": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings"
]
},
{
"activeLayer": -2,
"flipBoard": false,
"layers": [
6,
8,
10,
12,
14,
16,
18,
20,
22,
24,
25,
26,
28,
30,
32,
34,
36,
38,
40,
42,
44,
46,
48,
50,
52,
54,
56,
58,
60,
62
],
"name": "In.2 Cu - DVDD / AVDD / AVSS",
"renderLayers": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings"
]
},
{
"activeLayer": -2,
"flipBoard": false,
"layers": [
14,
16,
18,
20,
22,
24,
25,
26,
28,
30,
32,
34,
36,
37,
38,
40,
42,
44,
46,
48,
49,
50,
51,
52,
53,
54,
55,
56,
58,
60,
62
],
"name": "Layer 6 - x signal",
"renderLayers": [
"vias",
"footprint_text",
"footprint_anchors",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings"
]
},
{
"activeLayer": -2,
"flipBoard": false,
"layers": [
8,
10,
16,
18,
20,
22,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
40,
42,
44,
46,
48,
50,
52,
54,
56,
58,
60,
62
],
"name": "horiz-vert inner",
"renderLayers": [
"vias",
"footprint_text",
"footprint_anchors",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings"
]
}
],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"conflicting_netclasses": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "issue17429-9.0.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.127,
"diff_pair_gap": 0.127,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.154,
"line_style": 0,
"microvia_diameter": 0.254,
"microvia_drill": 0.4572,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.154,
"via_diameter": 0.4572,
"via_drill": 0.254,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
},
{
"group_by": false,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": false,
"name": "Grouped By Value",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"ngspice": {
"fix_include_paths": true,
"fix_passive_vals": false,
"meta": {
"version": 0
},
"model_mode": 0,
"workbook_filename": ""
},
"page_layout_descr_file": "empty.kicad_wks",
"plot_directory": "",
"space_save_all_events": true,
"spice_adjust_passive_values": false,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"9e566840-a51e-4471-a3e7-2b665aaf43dc",
"Root"
],
[
"3f8adf54-3826-4b3b-9b7c-b7e31dd15219",
"MIM 1 Connectors"
],
[
"959ea2eb-ede7-4845-a5e0-f5d704ded284",
"USB C Power Delivery and USB connectors"
],
[
"876907c5-4303-4331-b98b-edb1b9148224",
"Jupiter CM1 Connectors"
],
[
"b65d545f-09b3-49cc-ba93-08687c0e9554",
"SD Card , JTAG Connectors, UUID EEPROM"
],
[
"0836bfb4-6518-4cf8-bc77-6e6f74f1461f",
"Electrode Connector"
],
[
"7178ddd0-9526-4df3-bf3a-3aae1a195dc4",
"System LED and Accelerometer"
],
[
"f74cc5db-afe7-427c-9803-d67b8c3889e9",
"Battery Charger, Fuel Gauge, Regulators"
],
[
"b57c4078-aa32-4f39-b920-da21bd34b003",
"User Connectors and Optoisolation"
],
[
"1c95cdbc-eb19-4e2d-a613-dc22a92780a9",
"MIM 0 Connectors"
]
],
"text_variables": {}
}

View File

@ -84,9 +84,17 @@ void LoadBoard( SETTINGS_MANAGER& aSettingsManager, const wxString& aRelPath,
wxFileName rulesFile( absPath + ".kicad_dru" );
if( projectFile.Exists() )
{
aSettingsManager.LoadProject( projectFile.GetFullPath() );
BOOST_TEST_MESSAGE( "Loading project file: " << projectFile.GetFullPath() );
}
else if( legacyProject.Exists() )
{
aSettingsManager.LoadProject( legacyProject.GetFullPath() );
BOOST_TEST_MESSAGE( "Loading project file: " << projectFile.GetFullPath() );
}
else
BOOST_TEST_MESSAGE( "Could not load project: " << projectFile.GetFullPath() );
BOOST_TEST_MESSAGE( "Loading board file: " << boardPath );

View File

@ -65,6 +65,7 @@ set( QA_PCBNEW_SRCS
drc/test_drc_skew.cpp
drc/test_drc_component_classes.cpp
drc/test_drc_incorrect_text_mirror.cpp
drc/test_drc_unconnected_items_exclusion_loss.cpp
pcb_io/altium/test_altium_rule_transformer.cpp
pcb_io/altium/test_altium_pcblib_import.cpp

View File

@ -0,0 +1,330 @@
/*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <filesystem>
#include <iostream>
#include <string>
#include <board_design_settings.h>
#include <board.h>
#include <boost/test/unit_test.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid.hpp>
#include <drc/drc_item.h>
#include <footprint.h>
#include <pad.h>
#include <pcb_edit_frame.h>
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
#include <pcb_io/pcb_io_mgr.h>
#include <pcb_io/pcb_io.h>
#include <pcb_marker.h>
#include <pcb_track.h>
#include <pcbnew_utils/board_file_utils.h>
#include <pcbnew_utils/board_test_utils.h>
#include <project.h>
#include <project/project_file.h>
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <settings/settings_manager.h>
#include <tool/tool_manager.h>
#include <wx/string.h>
struct FileCleaner
{
std::vector<wxString> m_files_to_delete;
FileCleaner() = default;
~FileCleaner()
{
for( const auto& f_path : m_files_to_delete )
{
if( wxFileName::Exists( f_path ) )
{
if( !wxRemoveFile( f_path ) )
{
BOOST_TEST_MESSAGE( "Warning: Failed to delete temporary file " << f_path );
}
}
}
}
void AddFile( const wxString& f_path ) { m_files_to_delete.push_back( f_path ); }
};
struct DRC_BASE_FIXTURE
{
DRC_BASE_FIXTURE() :
m_settingsManager( true /* headless */ )
{
}
std::string generate_uuid();
bool SaveBoardToFile( BOARD* board, const wxString& filename );
void loadBoardAndVerifyInitialExclusions( const wxString& aBoardNameStem, int aExpectedInitialExclusions );
void createAndVerifyInitialExclusionMarkers();
int createAndVerifyAdditionalUnconnectedExclusions( int aAdditionalExclusions, int aInitialExclusions );
void runDrcOnBoard();
void saveBoardAndProjectToTempFiles( const wxString& aBoardNameStem, FileCleaner& aCleaner,
wxString& aTempBoardFullPath, wxString& aTempProjectFullPath,
wxString& aTempBoardStemName );
void reloadBoardAndVerifyExclusions( const wxString& aTempBoardStemName, int aExpectedExclusions );
SETTINGS_MANAGER m_settingsManager;
std::unique_ptr<BOARD> m_board;
};
struct DRC_REGRESSION_TEST_FIXTURE : public DRC_BASE_FIXTURE
{
DRC_REGRESSION_TEST_FIXTURE() :
DRC_BASE_FIXTURE()
{
}
};
struct DRC_UNCONNECTED_SAVE_FIXTURE : public DRC_BASE_FIXTURE
{
DRC_UNCONNECTED_SAVE_FIXTURE() :
DRC_BASE_FIXTURE()
{
m_board = std::make_unique<BOARD>();
}
};
std::string DRC_BASE_FIXTURE::generate_uuid()
{
boost::uuids::uuid uuid = boost::uuids::random_generator()();
return boost::uuids::to_string( uuid );
}
void DRC_BASE_FIXTURE::loadBoardAndVerifyInitialExclusions( const wxString& aBoardNameStem,
int aExpectedInitialExclusions )
{
KI_TEST::LoadBoard( m_settingsManager, aBoardNameStem, m_board );
BOOST_REQUIRE_MESSAGE( m_board,
"Could not load board " + aBoardNameStem ); // Ensure board loaded from test data directory
PROJECT* pcb_project = m_board->GetProject();
BOOST_REQUIRE_MESSAGE( pcb_project, "Get project pointer after initial loading." );
// Board test file comes with initial exclusions, check if they are preserved after loading
const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
size_t initialExclusionsCount = bds.m_DrcExclusions.size();
size_t initialExclusionsCommentsCount = bds.m_DrcExclusionComments.size();
BOOST_TEST_MESSAGE( "Initial DRC exclusions count: " << initialExclusionsCount );
BOOST_CHECK_EQUAL( initialExclusionsCount, (size_t) aExpectedInitialExclusions );
BOOST_TEST_MESSAGE( "Initial DRC exclusion comments count: " << initialExclusionsCommentsCount );
BOOST_CHECK_EQUAL( initialExclusionsCommentsCount, (size_t) aExpectedInitialExclusions );
}
void DRC_BASE_FIXTURE::createAndVerifyInitialExclusionMarkers()
{
const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
std::vector<PCB_MARKER*> markers;
for( const wxString exclusion : bds.m_DrcExclusions )
{
PCB_MARKER* marker = PCB_MARKER::DeserializeFromString( exclusion );
if( marker )
{
wxString comment = bds.m_DrcExclusionComments.at( exclusion );
marker->SetExcluded( true, comment );
markers.push_back( marker );
m_board->Add( marker );
}
}
size_t actualExclusionsCount = bds.m_DrcExclusions.size();
size_t initialExclusionsCount = markers.size();
BOOST_CHECK_EQUAL( actualExclusionsCount, initialExclusionsCount );
BOOST_TEST_MESSAGE( std::string( "Actual DRC exclusions count: " ) + std::to_string( actualExclusionsCount )
+ " after adding initial markers." );
}
int DRC_BASE_FIXTURE::createAndVerifyAdditionalUnconnectedExclusions( int aAdditionalExclusions,
int aInitialExclusions )
{
for( int i = 0; i < aAdditionalExclusions; ++i )
{
std::shared_ptr<DRC_ITEM> drcItem = DRC_ITEM::Create( DRCE_UNCONNECTED_ITEMS );
wxString id1 = wxString::Format( "12345678-1234-1234-1234-12345678%04d", i );
wxString id2 = wxString::Format( "87654321-4321-4321-4321-87654321%04d", i );
drcItem->SetItems( KIID( id1 ), KIID( id2 ) );
PCB_MARKER* marker = new PCB_MARKER( drcItem, VECTOR2I( 1000 * i, 1000 * i ) );
m_board->Add( marker );
// Exclude odd-numbered markers
if( i % 2 == 1 )
{
marker->SetExcluded( true, wxString::Format( "Exclusion %d", i ) );
}
}
// Store the new exclusion markers in the board
m_board->RecordDRCExclusions();
// Verify the number of exclusions after adding unconnected items
const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
const int expectedExclusions =
aInitialExclusions + aAdditionalExclusions / 2; // Only odd-numbered markers are excluded
size_t newActualExclusionsCount = bds.m_DrcExclusions.size();
BOOST_TEST_MESSAGE( std::string( "New actual DRC exclusions count: " ) + std::to_string( newActualExclusionsCount )
+ " after adding unconnected items." );
BOOST_CHECK_EQUAL( newActualExclusionsCount, (size_t) expectedExclusions );
return expectedExclusions;
}
void DRC_BASE_FIXTURE::runDrcOnBoard()
{
BOOST_TEST_MESSAGE( "Running DRC on board." );
BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
bds.m_DRCEngine->InitEngine( wxFileName() );
bool runDRC = true;
bool runDRCOnAllLayers = true;
bds.m_DRCEngine->RunTests( EDA_UNITS::MM, runDRC, runDRCOnAllLayers );
m_board->RecordDRCExclusions();
m_board->ResolveDRCExclusions( false );
BOOST_TEST_MESSAGE( "DRC done." );
}
void DRC_BASE_FIXTURE::saveBoardAndProjectToTempFiles( const wxString& aBoardNameStem, FileCleaner& aCleaner,
wxString& aTempBoardFullPath, wxString& aTempProjectFullPath,
wxString& aTempBoardStemName )
{
wxString tempPrefix = "tmp_test_drc_";
aTempBoardStemName = tempPrefix + aBoardNameStem.ToStdString();
aTempBoardFullPath = KI_TEST::GetPcbnewTestDataDir() + aTempBoardStemName + ".kicad_pcb";
aCleaner.AddFile( aTempBoardFullPath );
wxString tempProjectStemName = tempPrefix + aBoardNameStem.ToStdString();
aTempProjectFullPath = KI_TEST::GetPcbnewTestDataDir() + aTempBoardStemName + ".kicad_pro";
aCleaner.AddFile( aTempProjectFullPath );
bool boardSaved = SaveBoardToFile( m_board->GetBoard(), aTempBoardFullPath );
BOOST_REQUIRE_MESSAGE( boardSaved, "Save board to temporary file: " << aTempBoardFullPath );
m_settingsManager.SaveProjectAs( aTempProjectFullPath, m_board->GetProject() );
BOOST_REQUIRE_MESSAGE( wxFileName::Exists( aTempProjectFullPath ),
"Save project to temporary file: " << aTempProjectFullPath );
}
void DRC_BASE_FIXTURE::reloadBoardAndVerifyExclusions( const wxString& aTempBoardStemName, int aExpectedExclusions )
{
// clear the current board to ensure a fresh load
m_board.reset();
KI_TEST::LoadBoard( m_settingsManager, aTempBoardStemName, m_board );
BOOST_REQUIRE_MESSAGE( m_board, "Could not load board from tempfile:"
+ aTempBoardStemName ); // Ensure board loaded from test data directory
PROJECT* pcb_project = m_board->GetProject();
BOOST_REQUIRE_MESSAGE( pcb_project, "Get project pointer after initial loading." );
BOARD_DESIGN_SETTINGS& reloaded_bds = m_board->GetDesignSettings();
size_t reloadedExclusionsCount = reloaded_bds.m_DrcExclusions.size();
BOOST_TEST_MESSAGE( "Reloaded DRC exclusions count: " << reloadedExclusionsCount );
BOOST_CHECK_EQUAL( reloadedExclusionsCount, aExpectedExclusions );
}
bool DRC_BASE_FIXTURE::SaveBoardToFile( BOARD* board, const wxString& filename )
{
try
{
IO_RELEASER<PCB_IO> pi( PCB_IO_MGR::PluginFind( PCB_IO_MGR::KICAD_SEXP ) );
pi->SaveBoard( filename, board, nullptr );
return true;
}
catch( const IO_ERROR& error )
{
BOOST_TEST_MESSAGE( wxString::Format( "Save board to %s: %s", filename, error.What() ) );
return false;
}
}
BOOST_FIXTURE_TEST_CASE( DRCUnconnectedExclusionsLoss, DRC_UNCONNECTED_SAVE_FIXTURE )
{
// Test that unconnected item exclusions are not lost after multiple DRC runs.
// This test is expected to fail if the bug (issue17429) is present.
std::vector<std::pair<wxString, int>> tests = {
{ "issue17429", 10 }, // board name stem, expected initial exclusions
};
const int NUM_DRC_RUNS = 2;
for( const std::pair<wxString, int>& test_params : tests )
{
wxString boardNameStem = test_params.first;
int expectedInitialExclusions = test_params.second;
loadBoardAndVerifyInitialExclusions( boardNameStem, expectedInitialExclusions );
createAndVerifyInitialExclusionMarkers();
const int additionalExclusions = 5;
int expectedExclusions =
createAndVerifyAdditionalUnconnectedExclusions( additionalExclusions, expectedInitialExclusions );
runDrcOnBoard();
const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
BOOST_TEST_MESSAGE( std::string( "DRC exclusions count after DRC run: " ) + std::to_string( expectedExclusions )
+ " after adding unconnected items." );
BOOST_CHECK_EQUAL( bds.m_DrcExclusions.size(), expectedExclusions );
}
}
BOOST_FIXTURE_TEST_CASE( DRCUnconnectedItemsExclusionsSaveLoad, DRC_REGRESSION_TEST_FIXTURE )
{
namespace fs = std::filesystem;
// Test that unconnected item exclusions are not lost during save/load.
// This test is expected to fail if the bug (issue17429) is present.
std::vector<std::pair<wxString, int>> tests = {
{ "issue17429", 10 }, // board name stem, expected initial exclusions
};
for( const std::pair<wxString, int>& test_params : tests )
{
FileCleaner tempFileCleaner;
wxString boardNameStem = test_params.first;
int expectedInitialExclusions = test_params.second;
loadBoardAndVerifyInitialExclusions( boardNameStem, expectedInitialExclusions );
wxString tempBoardFullPath, tempProjectFullPath, tempBoardStemName;
saveBoardAndProjectToTempFiles( boardNameStem, tempFileCleaner, tempBoardFullPath, tempProjectFullPath,
tempBoardStemName );
createAndVerifyInitialExclusionMarkers();
const int additionalExclusions = 5;
int expectedExclusions =
createAndVerifyAdditionalUnconnectedExclusions( additionalExclusions, expectedInitialExclusions );
bool boardSaved = SaveBoardToFile( m_board->GetBoard(), tempBoardFullPath );
BOOST_REQUIRE_MESSAGE( boardSaved, "Save board to temporary file: " << tempBoardFullPath );
m_settingsManager.SaveProjectAs( tempProjectFullPath, m_board->GetProject() );
BOOST_REQUIRE_MESSAGE( wxFileName::Exists( tempProjectFullPath ),
"Save project to temporary file: " << tempProjectFullPath );
reloadBoardAndVerifyExclusions( tempBoardStemName, expectedExclusions );
}
}