1use std::collections::HashMap;
6
7use super::types::{
8 ContractKind, SolAnalysisCache, SolConstantFoldingHelper, SolDepGraph, SolDominatorTree,
9 SolExtCache, SolExtConstFolder, SolExtDepGraph, SolExtDomTree, SolExtLiveness,
10 SolExtPassConfig, SolExtPassPhase, SolExtPassRegistry, SolExtPassStats, SolExtWorklist,
11 SolLivenessInfo, SolPassConfig, SolPassPhase, SolPassRegistry, SolPassStats, SolWorklist,
12 SolidityBackend, SolidityContract, SolidityEnum, SolidityError, SolidityEvent, SolidityExpr,
13 SolidityFunction, SolidityParam, SolidityStateVar, SolidityStmt, SolidityStruct, SolidityType,
14 StateMutability, Visibility,
15};
16
17pub const SOLIDITY_RUNTIME: &str = r#"// SPDX-License-Identifier: MIT
20// OxiLean Solidity Runtime Library (inlined)
21
22/// @dev SafeMath-equivalent operations (Solidity 0.8+ has overflow checks built in)
23library OxiLeanMath {
24 /// @notice Returns the minimum of two values.
25 function min(uint256 a, uint256 b) internal pure returns (uint256) {
26 return a < b ? a : b;
27 }
28
29 /// @notice Returns the maximum of two values.
30 function max(uint256 a, uint256 b) internal pure returns (uint256) {
31 return a > b ? a : b;
32 }
33
34 /// @notice Returns the absolute difference.
35 function absDiff(uint256 a, uint256 b) internal pure returns (uint256) {
36 return a >= b ? a - b : b - a;
37 }
38
39 /// @notice Saturating addition (clamps to uint256 max).
40 function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {
41 unchecked {
42 uint256 c = a + b;
43 return c < a ? type(uint256).max : c;
44 }
45 }
46
47 /// @notice Saturating subtraction (clamps to 0).
48 function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {
49 return a > b ? a - b : 0;
50 }
51
52 /// @notice Integer square root (floor).
53 function sqrt(uint256 x) internal pure returns (uint256 r) {
54 if (x == 0) return 0;
55 r = 1;
56 uint256 xAux = x;
57 if (xAux >= 0x100000000000000000000000000000000) { r <<= 64; xAux >>= 128; }
58 if (xAux >= 0x10000000000000000) { r <<= 32; xAux >>= 64; }
59 if (xAux >= 0x100000000) { r <<= 16; xAux >>= 32; }
60 if (xAux >= 0x10000) { r <<= 8; xAux >>= 16; }
61 if (xAux >= 0x100) { r <<= 4; xAux >>= 8; }
62 if (xAux >= 0x10) { r <<= 2; xAux >>= 4; }
63 if (xAux >= 0x4) { r <<= 1; }
64 r = (r + x / r) >> 1;
65 r = (r + x / r) >> 1;
66 r = (r + x / r) >> 1;
67 r = (r + x / r) >> 1;
68 r = (r + x / r) >> 1;
69 r = (r + x / r) >> 1;
70 r = (r + x / r) >> 1;
71 return r > x / r ? r - 1 : r;
72 }
73}
74
75/// @dev Address utilities
76library OxiLeanAddress {
77 /// @notice Returns true if the address is the zero address.
78 function isZero(address addr) internal pure returns (bool) {
79 return addr == address(0);
80 }
81
82 /// @notice Converts an address to uint256.
83 function toUint256(address addr) internal pure returns (uint256) {
84 return uint256(uint160(addr));
85 }
86
87 /// @notice Converts uint256 to address.
88 function fromUint256(uint256 n) internal pure returns (address) {
89 return address(uint160(n));
90 }
91}
92
93/// @dev Bytes utilities
94library OxiLeanBytes {
95 /// @notice Converts bytes32 to bytes.
96 function toBytes(bytes32 b) internal pure returns (bytes memory) {
97 bytes memory result = new bytes(32);
98 assembly { mstore(add(result, 32), b) }
99 return result;
100 }
101
102 /// @notice Concatenates two byte arrays.
103 function concat(bytes memory a, bytes memory b) internal pure returns (bytes memory c) {
104 uint256 la = a.length;
105 uint256 lb = b.length;
106 c = new bytes(la + lb);
107 for (uint256 i = 0; i < la; i++) c[i] = a[i];
108 for (uint256 j = 0; j < lb; j++) c[la + j] = b[j];
109 }
110}
111"#;
112#[cfg(test)]
113mod tests {
114 use super::*;
115 #[test]
116 pub(super) fn test_uint256_display() {
117 assert_eq!(SolidityType::Uint256.to_string(), "uint256");
118 }
119 #[test]
120 pub(super) fn test_mapping_display() {
121 let ty = SolidityType::Mapping(
122 Box::new(SolidityType::Address),
123 Box::new(SolidityType::Uint256),
124 );
125 assert_eq!(ty.to_string(), "mapping(address => uint256)");
126 }
127 #[test]
128 pub(super) fn test_dyn_array_display() {
129 let ty = SolidityType::DynArray(Box::new(SolidityType::Uint256));
130 assert_eq!(ty.to_string(), "uint256[]");
131 }
132 #[test]
133 pub(super) fn test_fixed_array_display() {
134 let ty = SolidityType::FixedArray(Box::new(SolidityType::Bool), 10);
135 assert_eq!(ty.to_string(), "bool[10]");
136 }
137 #[test]
138 pub(super) fn test_string_type_display() {
139 assert_eq!(SolidityType::StringTy.to_string(), "string");
140 }
141 #[test]
142 pub(super) fn test_tuple_abi_canonical() {
143 let ty = SolidityType::Tuple(vec![
144 SolidityType::Uint256,
145 SolidityType::Address,
146 SolidityType::Bool,
147 ]);
148 assert_eq!(ty.abi_canonical(), "(uint256,address,bool)");
149 }
150 #[test]
151 pub(super) fn test_is_reference_type_string() {
152 assert!(SolidityType::StringTy.is_reference_type());
153 }
154 #[test]
155 pub(super) fn test_is_reference_type_uint256() {
156 assert!(!SolidityType::Uint256.is_reference_type());
157 }
158 #[test]
159 pub(super) fn test_is_reference_type_mapping() {
160 let ty = SolidityType::Mapping(
161 Box::new(SolidityType::Address),
162 Box::new(SolidityType::Uint256),
163 );
164 assert!(ty.is_reference_type());
165 }
166 #[test]
167 pub(super) fn test_param_new_value_type() {
168 let p = SolidityParam::new(SolidityType::Uint256, "amount");
169 assert!(p.location.is_none());
170 assert_eq!(p.to_string(), "uint256 amount");
171 }
172 #[test]
173 pub(super) fn test_param_new_reference_type() {
174 let p = SolidityParam::new(SolidityType::StringTy, "name");
175 assert_eq!(p.location.as_deref(), Some("memory"));
176 assert_eq!(p.to_string(), "string memory name");
177 }
178 #[test]
179 pub(super) fn test_param_calldata() {
180 let p = SolidityParam::calldata(SolidityType::Bytes, "data");
181 assert_eq!(p.location.as_deref(), Some("calldata"));
182 assert_eq!(p.to_string(), "bytes calldata data");
183 }
184 #[test]
185 pub(super) fn test_abi_signature() {
186 let mut func = SolidityFunction::new("transfer");
187 func.params
188 .push(SolidityParam::new(SolidityType::Address, "to"));
189 func.params
190 .push(SolidityParam::new(SolidityType::Uint256, "amount"));
191 assert_eq!(func.abi_signature(), "transfer(address,uint256)");
192 }
193 #[test]
194 pub(super) fn test_selector_length() {
195 let func = SolidityFunction::new("balanceOf");
196 let sel = func.selector();
197 assert_eq!(sel.len(), 4);
198 }
199 #[test]
200 pub(super) fn test_selector_deterministic() {
201 let f1 = SolidityFunction::new("approve");
202 let f2 = SolidityFunction::new("approve");
203 assert_eq!(f1.selector(), f2.selector());
204 }
205 #[test]
206 pub(super) fn test_msg_sender_display() {
207 assert_eq!(SolidityExpr::MsgSender.to_string(), "msg.sender");
208 }
209 #[test]
210 pub(super) fn test_binop_display() {
211 let expr = SolidityExpr::BinOp(
212 "+".into(),
213 Box::new(SolidityExpr::Var("a".into())),
214 Box::new(SolidityExpr::IntLit(1)),
215 );
216 assert_eq!(expr.to_string(), "(a + 1)");
217 }
218 #[test]
219 pub(super) fn test_ternary_display() {
220 let expr = SolidityExpr::Ternary(
221 Box::new(SolidityExpr::BoolLit(true)),
222 Box::new(SolidityExpr::IntLit(1)),
223 Box::new(SolidityExpr::IntLit(0)),
224 );
225 assert_eq!(expr.to_string(), "(true ? 1 : 0)");
226 }
227 #[test]
228 pub(super) fn test_keccak256_display() {
229 let expr = SolidityExpr::Keccak256(Box::new(SolidityExpr::Var("data".into())));
230 assert_eq!(expr.to_string(), "keccak256(data)");
231 }
232 #[test]
233 pub(super) fn test_type_max_display() {
234 let expr = SolidityExpr::TypeMax(SolidityType::Uint256);
235 assert_eq!(expr.to_string(), "type(uint256).max");
236 }
237 #[test]
238 pub(super) fn test_emit_empty_contract() {
239 let mut backend = SolidityBackend::new();
240 backend.add_contract(SolidityContract::new("Empty", ContractKind::Contract));
241 let src = backend.emit_contract();
242 assert!(src.contains("pragma solidity"));
243 assert!(src.contains("contract Empty {"));
244 assert!(src.contains("SPDX-License-Identifier: MIT"));
245 }
246 #[test]
247 pub(super) fn test_emit_interface() {
248 let mut backend = SolidityBackend::new();
249 let mut iface = SolidityContract::new("IERC20", ContractKind::Interface);
250 let mut func = SolidityFunction::new("totalSupply");
251 func.returns
252 .push(SolidityParam::new(SolidityType::Uint256, ""));
253 func.mutability = StateMutability::View;
254 func.body = vec![];
255 iface.functions.push(func);
256 backend.add_contract(iface);
257 let src = backend.emit_contract();
258 assert!(src.contains("interface IERC20 {"));
259 assert!(src.contains("function totalSupply()"));
260 }
261 #[test]
262 pub(super) fn test_emit_contract_with_state_var() {
263 let mut backend = SolidityBackend::new();
264 let mut contract = SolidityContract::new("Token", ContractKind::Contract);
265 contract.state_vars.push(SolidityStateVar {
266 ty: SolidityType::Mapping(
267 Box::new(SolidityType::Address),
268 Box::new(SolidityType::Uint256),
269 ),
270 name: "_balances".into(),
271 visibility: Visibility::Private,
272 is_immutable: false,
273 is_constant: false,
274 init: None,
275 doc: None,
276 });
277 backend.add_contract(contract);
278 let src = backend.emit_contract();
279 assert!(src.contains("mapping(address => uint256)"));
280 assert!(src.contains("_balances"));
281 }
282 #[test]
283 pub(super) fn test_emit_event() {
284 let mut backend = SolidityBackend::new();
285 let mut contract = SolidityContract::new("Token", ContractKind::Contract);
286 contract.events.push(SolidityEvent {
287 name: "Transfer".into(),
288 fields: vec![
289 (SolidityType::Address, true, "from".into()),
290 (SolidityType::Address, true, "to".into()),
291 (SolidityType::Uint256, false, "value".into()),
292 ],
293 anonymous: false,
294 doc: None,
295 });
296 backend.add_contract(contract);
297 let src = backend.emit_contract();
298 assert!(src.contains("event Transfer("));
299 assert!(src.contains("indexed from"));
300 assert!(src.contains("indexed to"));
301 }
302 #[test]
303 pub(super) fn test_emit_custom_error() {
304 let mut backend = SolidityBackend::new();
305 let mut contract = SolidityContract::new("Token", ContractKind::Contract);
306 contract.errors.push(SolidityError {
307 name: "InsufficientBalance".into(),
308 params: vec![
309 SolidityParam::new(SolidityType::Uint256, "available"),
310 SolidityParam::new(SolidityType::Uint256, "required"),
311 ],
312 doc: None,
313 });
314 backend.add_contract(contract);
315 let src = backend.emit_contract();
316 assert!(src.contains("error InsufficientBalance("));
317 }
318 #[test]
319 pub(super) fn test_emit_require_stmt() {
320 let stmt = SolidityStmt::Require(
321 SolidityExpr::BinOp(
322 ">".into(),
323 Box::new(SolidityExpr::Var("amount".into())),
324 Box::new(SolidityExpr::IntLit(0)),
325 ),
326 Some("Amount must be positive".into()),
327 );
328 let out = SolidityBackend::emit_stmt(&stmt, 2);
329 assert!(out.contains("require("));
330 assert!(out.contains("Amount must be positive"));
331 }
332 #[test]
333 pub(super) fn test_compile_decl() {
334 let mut backend = SolidityBackend::new();
335 let sv = backend.compile_decl("owner", SolidityType::Address);
336 assert_eq!(sv.name, "owner");
337 assert!(matches!(sv.ty, SolidityType::Address));
338 }
339 #[test]
340 pub(super) fn test_runtime_constant_not_empty() {
341 assert!(!SOLIDITY_RUNTIME.is_empty());
342 assert!(SOLIDITY_RUNTIME.contains("OxiLeanMath"));
343 }
344 #[test]
345 pub(super) fn test_visibility_display() {
346 assert_eq!(Visibility::Public.to_string(), "public");
347 assert_eq!(Visibility::External.to_string(), "external");
348 assert_eq!(Visibility::Private.to_string(), "private");
349 assert_eq!(Visibility::Internal.to_string(), "internal");
350 }
351 #[test]
352 pub(super) fn test_state_mutability_display() {
353 assert_eq!(StateMutability::View.to_string(), "view");
354 assert_eq!(StateMutability::Pure.to_string(), "pure");
355 assert_eq!(StateMutability::Payable.to_string(), "payable");
356 assert_eq!(StateMutability::NonPayable.to_string(), "");
357 }
358 #[test]
359 pub(super) fn test_emit_struct() {
360 let s = SolidityStruct {
361 name: "Position".into(),
362 fields: vec![
363 (SolidityType::Uint256, "x".into()),
364 (SolidityType::Uint256, "y".into()),
365 ],
366 doc: None,
367 };
368 let out = SolidityBackend::emit_struct(&s, 1);
369 assert!(out.contains("struct Position {"));
370 assert!(out.contains("uint256 x;"));
371 assert!(out.contains("uint256 y;"));
372 }
373 #[test]
374 pub(super) fn test_emit_enum() {
375 let e = SolidityEnum {
376 name: "State".into(),
377 variants: vec!["Created".into(), "Active".into(), "Closed".into()],
378 doc: None,
379 };
380 let out = SolidityBackend::emit_enum(&e, 1);
381 assert!(out.contains("enum State {"));
382 assert!(out.contains("Created"));
383 assert!(out.contains("Closed"));
384 }
385 #[test]
386 pub(super) fn test_emit_with_runtime() {
387 let mut backend = SolidityBackend::new().with_runtime();
388 backend.add_contract(SolidityContract::new("X", ContractKind::Contract));
389 let src = backend.emit_contract();
390 assert!(src.contains("OxiLeanMath"));
391 assert!(src.contains("saturatingAdd"));
392 }
393 #[test]
394 pub(super) fn test_emit_inheritance() {
395 let mut backend = SolidityBackend::new();
396 let mut contract = SolidityContract::new("MyToken", ContractKind::Contract);
397 contract.bases.push("ERC20".into());
398 contract.bases.push("Ownable".into());
399 backend.add_contract(contract);
400 let src = backend.emit_contract();
401 assert!(src.contains("contract MyToken is ERC20, Ownable {"));
402 }
403 #[test]
404 pub(super) fn test_address_payable_display() {
405 assert_eq!(SolidityType::AddressPayable.to_string(), "address payable");
406 }
407 #[test]
408 pub(super) fn test_payable_expr_display() {
409 let expr = SolidityExpr::Payable(Box::new(SolidityExpr::MsgSender));
410 assert_eq!(expr.to_string(), "payable(msg.sender)");
411 }
412}
413#[cfg(test)]
414mod Sol_infra_tests {
415 use super::*;
416 #[test]
417 pub(super) fn test_pass_config() {
418 let config = SolPassConfig::new("test_pass", SolPassPhase::Transformation);
419 assert!(config.enabled);
420 assert!(config.phase.is_modifying());
421 assert_eq!(config.phase.name(), "transformation");
422 }
423 #[test]
424 pub(super) fn test_pass_stats() {
425 let mut stats = SolPassStats::new();
426 stats.record_run(10, 100, 3);
427 stats.record_run(20, 200, 5);
428 assert_eq!(stats.total_runs, 2);
429 assert!((stats.average_changes_per_run() - 15.0).abs() < 0.01);
430 assert!((stats.success_rate() - 1.0).abs() < 0.01);
431 let s = stats.format_summary();
432 assert!(s.contains("Runs: 2/2"));
433 }
434 #[test]
435 pub(super) fn test_pass_registry() {
436 let mut reg = SolPassRegistry::new();
437 reg.register(SolPassConfig::new("pass_a", SolPassPhase::Analysis));
438 reg.register(SolPassConfig::new("pass_b", SolPassPhase::Transformation).disabled());
439 assert_eq!(reg.total_passes(), 2);
440 assert_eq!(reg.enabled_count(), 1);
441 reg.update_stats("pass_a", 5, 50, 2);
442 let stats = reg.get_stats("pass_a").expect("stats should exist");
443 assert_eq!(stats.total_changes, 5);
444 }
445 #[test]
446 pub(super) fn test_analysis_cache() {
447 let mut cache = SolAnalysisCache::new(10);
448 cache.insert("key1".to_string(), vec![1, 2, 3]);
449 assert!(cache.get("key1").is_some());
450 assert!(cache.get("key2").is_none());
451 assert!((cache.hit_rate() - 0.5).abs() < 0.01);
452 cache.invalidate("key1");
453 assert!(!cache.entries["key1"].valid);
454 assert_eq!(cache.size(), 1);
455 }
456 #[test]
457 pub(super) fn test_worklist() {
458 let mut wl = SolWorklist::new();
459 assert!(wl.push(1));
460 assert!(wl.push(2));
461 assert!(!wl.push(1));
462 assert_eq!(wl.len(), 2);
463 assert_eq!(wl.pop(), Some(1));
464 assert!(!wl.contains(1));
465 assert!(wl.contains(2));
466 }
467 #[test]
468 pub(super) fn test_dominator_tree() {
469 let mut dt = SolDominatorTree::new(5);
470 dt.set_idom(1, 0);
471 dt.set_idom(2, 0);
472 dt.set_idom(3, 1);
473 assert!(dt.dominates(0, 3));
474 assert!(dt.dominates(1, 3));
475 assert!(!dt.dominates(2, 3));
476 assert!(dt.dominates(3, 3));
477 }
478 #[test]
479 pub(super) fn test_liveness() {
480 let mut liveness = SolLivenessInfo::new(3);
481 liveness.add_def(0, 1);
482 liveness.add_use(1, 1);
483 assert!(liveness.defs[0].contains(&1));
484 assert!(liveness.uses[1].contains(&1));
485 }
486 #[test]
487 pub(super) fn test_constant_folding() {
488 assert_eq!(SolConstantFoldingHelper::fold_add_i64(3, 4), Some(7));
489 assert_eq!(SolConstantFoldingHelper::fold_div_i64(10, 0), None);
490 assert_eq!(SolConstantFoldingHelper::fold_div_i64(10, 2), Some(5));
491 assert_eq!(
492 SolConstantFoldingHelper::fold_bitand_i64(0b1100, 0b1010),
493 0b1000
494 );
495 assert_eq!(SolConstantFoldingHelper::fold_bitnot_i64(0), -1);
496 }
497 #[test]
498 pub(super) fn test_dep_graph() {
499 let mut g = SolDepGraph::new();
500 g.add_dep(1, 2);
501 g.add_dep(2, 3);
502 g.add_dep(1, 3);
503 assert_eq!(g.dependencies_of(2), vec![1]);
504 let topo = g.topological_sort();
505 assert_eq!(topo.len(), 3);
506 assert!(!g.has_cycle());
507 let pos: std::collections::HashMap<u32, usize> =
508 topo.iter().enumerate().map(|(i, &n)| (n, i)).collect();
509 assert!(pos[&1] < pos[&2]);
510 assert!(pos[&1] < pos[&3]);
511 assert!(pos[&2] < pos[&3]);
512 }
513}
514#[cfg(test)]
515mod solext_pass_tests {
516 use super::*;
517 #[test]
518 pub(super) fn test_solext_phase_order() {
519 assert_eq!(SolExtPassPhase::Early.order(), 0);
520 assert_eq!(SolExtPassPhase::Middle.order(), 1);
521 assert_eq!(SolExtPassPhase::Late.order(), 2);
522 assert_eq!(SolExtPassPhase::Finalize.order(), 3);
523 assert!(SolExtPassPhase::Early.is_early());
524 assert!(!SolExtPassPhase::Early.is_late());
525 }
526 #[test]
527 pub(super) fn test_solext_config_builder() {
528 let c = SolExtPassConfig::new("p")
529 .with_phase(SolExtPassPhase::Late)
530 .with_max_iter(50)
531 .with_debug(1);
532 assert_eq!(c.name, "p");
533 assert_eq!(c.max_iterations, 50);
534 assert!(c.is_debug_enabled());
535 assert!(c.enabled);
536 let c2 = c.disabled();
537 assert!(!c2.enabled);
538 }
539 #[test]
540 pub(super) fn test_solext_stats() {
541 let mut s = SolExtPassStats::new();
542 s.visit();
543 s.visit();
544 s.modify();
545 s.iterate();
546 assert_eq!(s.nodes_visited, 2);
547 assert_eq!(s.nodes_modified, 1);
548 assert!(s.changed);
549 assert_eq!(s.iterations, 1);
550 let e = s.efficiency();
551 assert!((e - 0.5).abs() < 1e-9);
552 }
553 #[test]
554 pub(super) fn test_solext_registry() {
555 let mut r = SolExtPassRegistry::new();
556 r.register(SolExtPassConfig::new("a").with_phase(SolExtPassPhase::Early));
557 r.register(SolExtPassConfig::new("b").disabled());
558 assert_eq!(r.len(), 2);
559 assert_eq!(r.enabled_passes().len(), 1);
560 assert_eq!(r.passes_in_phase(&SolExtPassPhase::Early).len(), 1);
561 }
562 #[test]
563 pub(super) fn test_solext_cache() {
564 let mut c = SolExtCache::new(4);
565 assert!(c.get(99).is_none());
566 c.put(99, vec![1, 2, 3]);
567 let v = c.get(99).expect("v should be present in map");
568 assert_eq!(v, &[1u8, 2, 3]);
569 assert!(c.hit_rate() > 0.0);
570 assert_eq!(c.live_count(), 1);
571 }
572 #[test]
573 pub(super) fn test_solext_worklist() {
574 let mut w = SolExtWorklist::new(10);
575 w.push(5);
576 w.push(3);
577 w.push(5);
578 assert_eq!(w.len(), 2);
579 assert!(w.contains(5));
580 let first = w.pop().expect("first should be available to pop");
581 assert!(!w.contains(first));
582 }
583 #[test]
584 pub(super) fn test_solext_dom_tree() {
585 let mut dt = SolExtDomTree::new(5);
586 dt.set_idom(1, 0);
587 dt.set_idom(2, 0);
588 dt.set_idom(3, 1);
589 dt.set_idom(4, 1);
590 assert!(dt.dominates(0, 3));
591 assert!(dt.dominates(1, 4));
592 assert!(!dt.dominates(2, 3));
593 assert_eq!(dt.depth_of(3), 2);
594 }
595 #[test]
596 pub(super) fn test_solext_liveness() {
597 let mut lv = SolExtLiveness::new(3);
598 lv.add_def(0, 1);
599 lv.add_use(1, 1);
600 assert!(lv.var_is_def_in_block(0, 1));
601 assert!(lv.var_is_used_in_block(1, 1));
602 assert!(!lv.var_is_def_in_block(1, 1));
603 }
604 #[test]
605 pub(super) fn test_solext_const_folder() {
606 let mut cf = SolExtConstFolder::new();
607 assert_eq!(cf.add_i64(3, 4), Some(7));
608 assert_eq!(cf.div_i64(10, 0), None);
609 assert_eq!(cf.mul_i64(6, 7), Some(42));
610 assert_eq!(cf.and_i64(0b1100, 0b1010), 0b1000);
611 assert_eq!(cf.fold_count(), 3);
612 assert_eq!(cf.failure_count(), 1);
613 }
614 #[test]
615 pub(super) fn test_solext_dep_graph() {
616 let mut g = SolExtDepGraph::new(4);
617 g.add_edge(0, 1);
618 g.add_edge(1, 2);
619 g.add_edge(2, 3);
620 assert!(!g.has_cycle());
621 assert_eq!(g.topo_sort(), Some(vec![0, 1, 2, 3]));
622 assert_eq!(g.reachable(0).len(), 4);
623 let sccs = g.scc();
624 assert_eq!(sccs.len(), 4);
625 }
626}