Skip to content

Commit 0a9a62d

Browse files
committed
Merge remote-tracking branch 'private/master' into dpl-cell-height
2 parents d010bc3 + 9f87d49 commit 0a9a62d

16 files changed

Lines changed: 377 additions & 54 deletions

File tree

docs/agents/testing-strategy.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ where algorithmic correctness, edge cases, and regression-bug pins belong.
6363
Their *only* job is to prove the Tcl/Python binding marshals arguments and
6464
returns without crashing -- not to validate the algorithm. Call the command once
6565
on a trivial design and assert it ran. Prefer `PASSFAIL_TESTS` (exit-code only)
66-
over golden diffs so there is no `.ok` file to maintain.
66+
over golden diffs so there is no `.ok` file to maintain. For commands whose
67+
execution is expensive (e.g. `detailed_route`), running them even once is the
68+
wrong cost trade-off -- see [Binding tests for expensive
69+
commands](#binding-tests-for-expensive-commands) for how to prove translation
70+
without executing the work.
6771

6872
### Tier 3 -- Full-flow integration tests (keep deliberately few)
6973

@@ -135,13 +139,98 @@ The existing fixture stack already supports this:
135139
`.tcl`/`.i` files and flag any without a smoke test, guaranteeing binding
136140
coverage without hand-curation.
137141

142+
### Binding tests for expensive commands
143+
144+
The Tier-2 recipe -- "call the command once on a trivial design" -- assumes the
145+
command is cheap to run. For commands that do heavy work (`global_route`,
146+
`global_placement`, `clock_tree_synthesis`, `detailed_route`, ...), executing the
147+
algorithm contributes *nothing* to the binding guarantee and costs
148+
seconds-to-minutes per test. The translation contract you actually want to pin is
149+
narrow: every flag/key reaches the right C++ parameter and defaults are applied.
150+
None of that requires the algorithm to run.
151+
152+
**Preferred policy: validate arguments in C++, not in the binding.** A value
153+
check written in a `.tcl` proc (`sta::check_positive_integer`, range/cardinality
154+
guards) only protects the Tcl entry point -- the Python binding and direct C++
155+
callers bypass it, so the check has to be duplicated or is simply missing. Put
156+
the check behind the C++ entry point instead and one implementation covers all
157+
three usages. `utl::Validator` (`src/utl/include/utl/validation.h`) exists to make
158+
this easy: construct it with a `Logger*` and `ToolId`, then call
159+
`check_positive` / `check_non_negative` / `check_range` / `check_percentage` /
160+
`check_non_null`, each of which emits a tool-scoped logged error on violation. Use
161+
it in the engine's argument-ingestion path (e.g. where parameters are set) rather
162+
than re-deriving the same guard per language. This also keeps the `.tcl`/`.py`
163+
proc to near-pure marshaling, which shrinks what the binding test must cover --
164+
and the validation itself becomes a cheap Tier-1 C++ test that exercises the error
165+
paths directly, with no process launch.
166+
167+
The key observation is that a command's .tcl proc or .py function does two separable
168+
things: it **configures** the engine from the parsed arguments, then calls a
169+
distinct **execute** entry point that does the expensive work. The execute step
170+
is almost always a single thin SWIG free function -- `grt::global_route`,
171+
`cts::run_triton_cts`, the `gpl::replace_*_cmd` calls. Because it is a plain proc
172+
in the tool's namespace, a binding test can rename it (in Tcl) or reassign/mock it (in Python) to a no-op spy, then
173+
invoke the *real* public command. All the argument handling runs; the engine does
174+
not, so the test is sub-millisecond.
175+
176+
Mind the proc's preconditions, though. Invoking the real command also runs any
177+
guards that sit *before* the execute call, and many commands require a loaded
178+
design: `global_route` errors `GRT-0051`/`GRT-0052` on a missing tech/block
179+
(`src/grt/src/GlobalRouter.tcl`) and `clock_tree_synthesis` errors `CTS-0103` on
180+
a missing block (`src/cts/src/TritonCTS.tcl`) before their execute calls are ever
181+
reached. So a no-design spy test for those fails on the guard, not on the spy.
182+
Give the test the *minimal* DB the proc's preconditions demand -- a tiny LEF/DEF
183+
or a `SimpleDbFixture`-style block is enough, since the expensive *algorithm*
184+
still never runs. (A command with no such precondition can be spied with no
185+
design loaded at all.) If you instead want to assert
186+
that a precondition guard itself fires, that is a separate, cheaper test: invoke
187+
the command with the precondition unmet and check the error code -- no spy needed,
188+
because the guard errors out before the execute call regardless.
189+
190+
What you assert depends on where the configure logic lives, which varies by
191+
command:
192+
193+
- **Setters, then a separate execute (e.g. `global_route`, `clock_tree_synthesis`).**
194+
The proc translates each flag/key into its own cheap `set_*` SWIG call
195+
(`grt::set_infinite_cap`, `cts::set_insertion_delay`, ...) before the execute
196+
call. Spy *only* the execute; let the setters run for real and assert the
197+
resulting configured state via getters (or spy the individual setters and check
198+
they were called with the right values). This is the most common shape.
199+
- **Arguments forwarded to C++ (e.g. `global_placement`).** The proc passes the
200+
raw key/flag arrays to a C++ command function that parses them itself. This is
201+
the shape the validate-in-C++ policy points toward: parsing *and* `utl::Validator`
202+
checks live in one place that all bindings share, so a C++ unit test on that
203+
parsing/validation is the natural binding check; spying the execute still lets
204+
the proc reach it without running the placer.
205+
- **Configure and execute fused (e.g. `detailed_route`).** A single
206+
`detailed_route_cmd` both marshals its arguments and calls `main()`. Spying it
207+
skips the run, so capture the arguments the spy received and assert them. Better,
208+
split the marshaling (setParams) from execution (main) so the cheap part is
209+
independently reachable -- this is the refactor that makes the command match the
210+
others, and lets a C++ test assert a `getParams()` round-trip directly.
211+
212+
In every case the heavy `main()`/run is off the translation path, so the test
213+
costs nothing at runtime. As with cheap commands, the C++ unit test remains the
214+
source of truth for algorithmic correctness -- do not assert behavior here.
215+
216+
The reusable design principle: **validate arguments in C++, and keep the
217+
expensive execute step as its own thin free function distinct from argument
218+
handling.** Validation in C++ covers Tcl, Python, and C++ callers from one place;
219+
a separate execute step keeps the heavy work off the translation path. Most
220+
commands already separate execute; a fused entry point like `detailed_route_cmd`
221+
is the outlier worth refactoring. Commands built this way are cheap to
222+
binding-test regardless of how expensive their execution is. (Free-function entry
223+
points also matter because a method on a SWIG object is much harder to intercept
224+
than a namespaced proc.)
225+
138226
## Decision tree for a new test
139227

140228
```
141229
Is it pure logic with no DB? -> Tier 1, no fixture
142230
Does it need an odb DB only? -> Tier 1, tst::Fixture / SimpleDbFixture
143231
Does it need STA/resizer/router state? -> Tier 1, tst::IntegratedFixture
144232
Is it only proving a binding marshals? -> Tier 2, smoke test (PASSFAIL)
233+
...and the command is expensive to run? -> Tier 2, intercept the C++ entry (no execution)
145234
Does it genuinely span multiple tools? -> Tier 3, flow test (golden, used sparingly)
146235
```
147236

src/dbSta/include/db_sta/dbNetwork.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class dbNetwork : public ConcreteNetwork
9393

9494
LibertyCell* libertyCell(Cell* cell) const override;
9595
const LibertyCell* libertyCell(const Cell* cell) const override;
96+
const LibertyCell* testCell(const Cell* cell) const;
9697
LibertyCell* libertyCell(odb::dbInst* inst);
9798
LibertyPort* libertyPort(const Pin*) const override;
9899
odb::dbInst* staToDb(const Instance* instance) const;
@@ -521,7 +522,6 @@ class dbNetwork : public ConcreteNetwork
521522
static constexpr unsigned DBIDTAG_WIDTH = 0x4;
522523

523524
private:
524-
const LibertyCell* getLibertyCell(const Cell* cell) const;
525525
void addDriverToCacheIfPresent(const Net* net, const Pin* drvr);
526526
void removeDriverFromCache(const Net* net);
527527
void removeDriverFromCache(const Net* net, const Pin* drvr);

src/dbSta/src/dbNetwork.cc

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3496,7 +3496,7 @@ LibertyCell* dbNetwork::libertyCell(dbInst* inst)
34963496
return libertyCell(dbToSta(inst));
34973497
}
34983498

3499-
const LibertyCell* dbNetwork::getLibertyCell(const Cell* cell) const
3499+
const LibertyCell* dbNetwork::testCell(const Cell* cell) const
35003500
{
35013501
const LibertyCell* lib_cell = libertyCell(cell);
35023502
if (!lib_cell) {
@@ -5280,7 +5280,7 @@ bool dbNetwork::isClockPin(odb::dbITerm* iterm) const
52805280
bool dbNetwork::clockOn(odb::dbInst* inst) const
52815281
{
52825282
const Cell* cell = dbToSta(inst->getMaster());
5283-
const LibertyCell* lib_cell = getLibertyCell(cell);
5283+
const LibertyCell* lib_cell = testCell(cell);
52845284
if (!lib_cell) {
52855285
return false;
52865286
}
@@ -5359,7 +5359,7 @@ int dbNetwork::getNumD(odb::dbInst* inst) const
53595359
{
53605360
int cnt_d = 0;
53615361
const Cell* cell = dbToSta(inst->getMaster());
5362-
const LibertyCell* lib_cell = getLibertyCell(cell);
5362+
const LibertyCell* lib_cell = testCell(cell);
53635363
if (lib_cell == nullptr) {
53645364
return 0;
53655365
}
@@ -5423,7 +5423,7 @@ bool dbNetwork::isInvertingQPin(odb::dbITerm* iterm) const
54235423
{
54245424
odb::dbInst* inst = iterm->getInst();
54255425
const Cell* cell = dbToSta(inst->getMaster());
5426-
const LibertyCell* lib_cell = getLibertyCell(cell);
5426+
const LibertyCell* lib_cell = testCell(cell);
54275427
if (!lib_cell) {
54285428
return false;
54295429
}
@@ -5473,7 +5473,7 @@ int dbNetwork::getNumQ(odb::dbInst* inst) const
54735473
bool dbNetwork::hasClear(odb::dbInst* inst) const
54745474
{
54755475
const Cell* cell = dbToSta(inst->getMaster());
5476-
const LibertyCell* lib_cell = getLibertyCell(cell);
5476+
const LibertyCell* lib_cell = testCell(cell);
54775477
if (!lib_cell) {
54785478
return false;
54795479
}
@@ -5507,7 +5507,7 @@ bool dbNetwork::isClearPin(odb::dbITerm* iterm) const
55075507
bool dbNetwork::hasPreset(odb::dbInst* inst) const
55085508
{
55095509
const Cell* cell = dbToSta(inst->getMaster());
5510-
const LibertyCell* lib_cell = getLibertyCell(cell);
5510+
const LibertyCell* lib_cell = testCell(cell);
55115511
if (!lib_cell) {
55125512
return false;
55135513
}
@@ -5542,15 +5542,15 @@ bool dbNetwork::isPresetPin(odb::dbITerm* iterm) const
55425542
bool dbNetwork::isScanCell(odb::dbInst* inst) const
55435543
{
55445544
const Cell* cell = dbToSta(inst->getMaster());
5545-
const LibertyCell* lib_cell = getLibertyCell(cell);
5545+
const LibertyCell* lib_cell = testCell(cell);
55465546
return lib_cell && getLibertyScanIn(lib_cell)
55475547
&& getLibertyScanEnable(lib_cell);
55485548
}
55495549

55505550
bool dbNetwork::isScanIn(odb::dbITerm* iterm) const
55515551
{
55525552
const Cell* cell = dbToSta(iterm->getInst()->getMaster());
5553-
const LibertyCell* lib_cell = getLibertyCell(cell);
5553+
const LibertyCell* lib_cell = testCell(cell);
55545554
if (lib_cell && getLibertyScanIn(lib_cell)) {
55555555
odb::dbMTerm* mterm = staToDb(getLibertyScanIn(lib_cell));
55565556
return iterm->getInst()->getITerm(mterm) == iterm;
@@ -5561,7 +5561,7 @@ bool dbNetwork::isScanIn(odb::dbITerm* iterm) const
55615561
odb::dbITerm* dbNetwork::getScanIn(odb::dbInst* inst) const
55625562
{
55635563
const Cell* cell = dbToSta(inst->getMaster());
5564-
const LibertyCell* lib_cell = getLibertyCell(cell);
5564+
const LibertyCell* lib_cell = testCell(cell);
55655565
if (lib_cell && getLibertyScanIn(lib_cell)) {
55665566
odb::dbMTerm* mterm = staToDb(getLibertyScanIn(lib_cell));
55675567
return inst->getITerm(mterm);
@@ -5572,7 +5572,7 @@ odb::dbITerm* dbNetwork::getScanIn(odb::dbInst* inst) const
55725572
bool dbNetwork::isScanEnable(odb::dbITerm* iterm) const
55735573
{
55745574
const Cell* cell = dbToSta(iterm->getInst()->getMaster());
5575-
const LibertyCell* lib_cell = getLibertyCell(cell);
5575+
const LibertyCell* lib_cell = testCell(cell);
55765576
if (lib_cell && getLibertyScanEnable(lib_cell)) {
55775577
odb::dbMTerm* mterm = staToDb(getLibertyScanEnable(lib_cell));
55785578
return (iterm->getInst()->getITerm(mterm) == iterm);
@@ -5583,7 +5583,7 @@ bool dbNetwork::isScanEnable(odb::dbITerm* iterm) const
55835583
odb::dbITerm* dbNetwork::getScanEnable(odb::dbInst* inst) const
55845584
{
55855585
const Cell* cell = dbToSta(inst->getMaster());
5586-
const LibertyCell* lib_cell = getLibertyCell(cell);
5586+
const LibertyCell* lib_cell = testCell(cell);
55875587
if (lib_cell && getLibertyScanEnable(lib_cell)) {
55885588
odb::dbMTerm* mterm = staToDb(getLibertyScanEnable(lib_cell));
55895589
return inst->getITerm(mterm);
@@ -5602,7 +5602,7 @@ bool dbNetwork::isValidFlop(odb::dbInst* FF) const
56025602
if (cell == nullptr) {
56035603
return false;
56045604
}
5605-
const LibertyCell* lib_cell = getLibertyCell(cell);
5605+
const LibertyCell* lib_cell = testCell(cell);
56065606
if (lib_cell == nullptr || !lib_cell->hasSequentials()) {
56075607
return false;
56085608
}
@@ -5642,7 +5642,7 @@ bool dbNetwork::isValidTray(odb::dbInst* tray) const
56425642
if (cell == nullptr) {
56435643
return false;
56445644
}
5645-
const LibertyCell* lib_cell = getLibertyCell(cell);
5645+
const LibertyCell* lib_cell = testCell(cell);
56465646
if (lib_cell == nullptr || !lib_cell->hasSequentials()) {
56475647
return false;
56485648
}

src/gpl/src/mbff.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ MBFF::PortName MBFF::PortType(const sta::LibertyPort* lib_port, dbInst* inst)
207207
}
208208

209209
const sta::Cell* cell = network_->dbToSta(inst->getMaster());
210-
const sta::LibertyCell* lib_cell = network_->libertyCell(cell);
210+
const sta::LibertyCell* lib_cell = network_->testCell(cell);
211211
for (const sta::Sequential& seq : lib_cell->sequentials()) {
212212
// function
213213
if (sta::LibertyPort::equiv(lib_port, seq.output())) {
@@ -280,7 +280,7 @@ MBFF::Mask MBFF::GetArrayMask(dbInst* inst, const bool isTray)
280280
}
281281

282282
const sta::Cell* cell = network_->dbToSta(inst->getMaster());
283-
const sta::LibertyCell* lib_cell = network_->libertyCell(cell);
283+
const sta::LibertyCell* lib_cell = network_->testCell(cell);
284284
const auto& seqs = lib_cell->sequentials();
285285
if (!seqs.empty()) {
286286
ret.func_idx = GetMatchingFunc(seqs.front().data(), inst, isTray);
@@ -294,7 +294,7 @@ MBFF::Mask MBFF::GetArrayMask(dbInst* inst, const bool isTray)
294294
MBFF::DataToOutputsMap MBFF::GetPinMapping(dbInst* tray)
295295
{
296296
const sta::Cell* cell = network_->dbToSta(tray->getMaster());
297-
const sta::LibertyCell* lib_cell = network_->libertyCell(cell);
297+
const sta::LibertyCell* lib_cell = network_->testCell(cell);
298298
sta::LibertyCellPortIterator port_itr(lib_cell);
299299

300300
std::vector<const sta::LibertyPort*> d_pins;

src/gpl/test/mbff_test.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class MBFFTestPeer
3131
{
3232
return uut->network_->isValidTray(tray);
3333
}
34+
static void ReadLibs(MBFF* uut) { uut->ReadLibs(); }
3435
};
3536

3637
namespace {
@@ -146,5 +147,13 @@ TEST_F(MBFFTestFixture, FlopsCanBeIdentifiedAsATrayAndNot)
146147
mbff_.get(), CreateTmpCell("test_tray", "test0", "MBFF2SECLPS")));
147148
}
148149

150+
TEST_F(MBFFTestFixture, ReadLibsSuccessfullyProcessesTestCells)
151+
{
152+
// In test0.lib, cells like MBFF2SE have their sequential definition
153+
// nested inside a test_cell block. Without consistent Liberty cell views,
154+
// GetPinMapping returns empty vectors and triggers an out-of-bounds crash.
155+
EXPECT_NO_FATAL_FAILURE(MBFFTestPeer::ReadLibs(mbff_.get()));
156+
}
157+
149158
} // namespace
150159
} // namespace gpl

src/pdn/test/core_grid_with_M6_min_area_rule.tcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ add_pdn_connect -layers {metal4 metal7}
2222

2323
pdngen
2424

25-
set def_file [make_result_file core_grid_with_M6_min_area.def]
25+
set def_file [make_result_file core_grid_with_M6_min_area_rule.def]
2626
write_def $def_file
2727
diff_files core_grid_with_M6_min_area.defok $def_file

src/syn/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ cc_library(
2323
"src/flow/export.cc",
2424
"src/flow/export.h",
2525
"src/flow/import.cc",
26-
"src/flow/import.h",
2726
"src/flow/liveness.cc",
2827
"src/flow/opt_gatefusion.cc",
2928
"src/flow/opt_gatefusion.h",
@@ -33,6 +32,7 @@ cc_library(
3332
hdrs = [
3433
"include/syn/synthesis.h",
3534
"src/flow/constant_fold.h",
35+
"src/flow/import.h",
3636
],
3737
includes = [
3838
"include",

src/syn/src/elab/driver.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ std::unique_ptr<Graph> elaborate(utl::Logger* logger,
3232
const std::vector<std::string>& args,
3333
sta::dbSta* sta = nullptr);
3434
std::unique_ptr<Graph> elaborateText(const std::string& source,
35-
const std::vector<std::string>& args = {});
35+
const std::vector<std::string>& args = {},
36+
sta::dbSta* sta = nullptr);
3637

3738
} // namespace syn

src/syn/src/elab/error.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ std::unique_ptr<Graph> elaborateImpl(utl::Logger* logger,
2929
std::optional<std::string> buffer);
3030

3131
std::unique_ptr<Graph> elaborateText(const std::string& source,
32-
const std::vector<std::string>& args)
32+
const std::vector<std::string>& args,
33+
sta::dbSta* sta)
3334
{
3435
utl::Logger logger;
35-
return elaborateImpl(&logger, args, nullptr, source);
36+
return elaborateImpl(&logger, args, sta, source);
3637
}
3738

3839
} // namespace syn

src/syn/src/flow/combinational_mapper.cc

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -490,15 +490,16 @@ ClassMatch* Mapping::allocMatches(const int n)
490490

491491
void Mapping::collectPrimaryOutputs()
492492
{
493-
std::set<ControlNet> primaryOutputNets;
494-
std::set<std::pair<ControlNet, Net>> fixupSet;
493+
std::set<ControlNet> primary_output_nets;
494+
std::set<std::pair<ControlNet, Net>> fixup_set;
495495

496496
auto registerPrimaryOutput = [&](Net net) {
497497
auto cnet = subject_.stripInverter(net);
498-
if (isMappable(cnet.net())) {
499-
primaryOutputNets.insert(cnet);
498+
auto driver = subject_.graph.resolve(cnet.net()).first;
499+
if (isMappable(cnet.net()) || driver->isMapped()) {
500+
primary_output_nets.insert(cnet);
500501
if (cnet.net() != net) {
501-
fixupSet.insert({cnet, net});
502+
fixup_set.insert({cnet, net});
502503
}
503504
}
504505
};
@@ -529,8 +530,9 @@ void Mapping::collectPrimaryOutputs()
529530
}
530531
});
531532

532-
primary_outputs_.assign(primaryOutputNets.begin(), primaryOutputNets.end());
533-
primary_output_fixups_.assign(fixupSet.begin(), fixupSet.end());
533+
primary_outputs_.assign(primary_output_nets.begin(),
534+
primary_output_nets.end());
535+
primary_output_fixups_.assign(fixup_set.begin(), fixup_set.end());
534536
}
535537

536538
void Mapping::computeFanouts()

0 commit comments

Comments
 (0)