Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 3.1.0 - 2025-03-10
## 4.0.0 - Unreleased

### Added

- **Testing**: Exhaustive property-based testing using `qcheck` across core algorithms (pathfinding, connectivity, MST) and properties.

## 3.1.0 - 2026-03-10

### Breaking

Expand Down
181 changes: 181 additions & 0 deletions PROPERTY_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Property-Based Testing Reference

Property-based testing for Yog graph algorithms using `qcheck` v1.0.4.

## Test Statistics

| Metric | Count |
|--------|-------|
| Total tests | 950 |
| Property tests | 34 |
| Basic properties | 12 |
| Edge case tests | 8 |
| Algorithm tests | 14 |

## Running Tests

```bash
gleam test
```

## Test Files

| File | Lines | Purpose |
|------|-------|---------|
| `test/yog/property_tests.gleam` | 462 | Basic structural properties |
| `test/yog/aggressive_property_tests.gleam` | 216 | Edge cases and boundary conditions |
| `test/yog/algorithm_property_tests.gleam` | 466 | Algorithm correctness and cross-validation |

## Category 1: Structural Properties

### Graph Transformations

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 1 | Transpose is involutive: `transpose(transpose(G)) = G` | `transpose_involutive_test()` | Validates O(1) transpose implementation used by SCC algorithms | ✅ |
| 2 | Edge count consistency | `edge_count_consistency_test()` | Ensures graph statistics match actual edge storage | ✅ |
| 3 | Undirected graphs are symmetric | `undirected_symmetry_test()` | For undirected graphs, every edge appears in both directions | ✅ |
| 4 | Neighbors equals successors (undirected) | `undirected_neighbors_equal_successors_test()` | API consistency for undirected graphs | ✅ |

### Data Transformations

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 5 | `map_nodes` preserves structure | `map_nodes_preserves_structure_test()` | Node transformations don't alter graph topology | ✅ |
| 6 | `map_edges` preserves structure | `map_edges_preserves_structure_test()` | Edge transformations don't alter graph topology | ✅ |
| 7 | `filter_nodes` removes incident edges | `filter_nodes_removes_incident_edges_test()` | No dangling edge references after node removal | ✅ |
| 8 | `to_undirected` creates symmetry | `to_undirected_creates_symmetry_test()` | Directed to undirected conversion adds reverse edges | ✅ |

### Operations

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 9 | Add/remove edge (directed) | `add_remove_edge_inverse_directed_test()` | Edge operations are inverse for directed graphs | ✅ |
| 10 | Add/remove edge (undirected) | `add_remove_edge_inverse_undirected_test()` | Documents asymmetric behavior (v3.x) | ✅ ⚠️ |

**Note on Property 10:** Currently documents known asymmetry where `remove_edge` only removes one direction for undirected graphs. Planned fix in v4.0.

### Traversals

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 11 | BFS produces no duplicates | `traversal_no_duplicates_bfs_test()` | Breadth-first search visits each node once | ✅ |
| 12 | DFS produces no duplicates | `traversal_no_duplicates_dfs_test()` | Depth-first search visits each node once, even with cycles | ✅ |

## Category 2: Edge Cases

| # | Case | Test Function | Rationale | Status |
|---|------|---------------|-----------|--------|
| 1 | Empty graphs | `empty_graph_edge_count_test()`, `empty_graph_transpose_test()` | Operations on graphs with no nodes/edges | ✅ |
| 2 | Self-loops (directed) | `self_loop_directed_test()` | Node pointing to itself in directed graph | ✅ |
| 3 | Self-loops (undirected) | `self_loop_undirected_test()` | Node pointing to itself in undirected graph | ✅ |
| 4 | Multiple edges same pair | `multiple_edges_same_pair_test()` | Duplicate edge insertion replaces weight | ✅ |
| 5 | Remove nonexistent edge | `remove_nonexistent_edge_test()` | Removing missing edge is no-op | ✅ |
| 6 | Undirected edge removal asymmetry | `undirected_edge_removal_asymmetry_test()` | Documents v3.x behavior requiring two removals | ✅ ⚠️ |
| 7 | Filter all nodes | `filter_all_nodes_test()` | Filtering removes all nodes and edges | ✅ |
| 8 | Transpose with self-loop | `transpose_with_self_loop_test()` | Self-loops remain after transpose | ✅ |
| 9 | Isolated nodes | `isolated_node_test()` | Nodes with no incoming/outgoing edges | ✅ |

## Category 3: Algorithm Correctness

### Cross-Validation

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 1 | Tarjan SCC = Kosaraju SCC | `scc_tarjan_equals_kosaraju_test()` | Different SCC algorithms produce same components | ✅ |
| 2 | Kruskal MST weight = Prim MST weight | `mst_kruskal_equals_prim_weight_test()` | Different MST algorithms produce same total weight | ✅ |
| 3 | Bellman-Ford = Dijkstra (non-negative) | `bellman_ford_equals_dijkstra_test()` | Algorithms agree on non-negative weighted graphs | ✅ |

### Pathfinding Correctness

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 4 | Dijkstra path validity | `dijkstra_path_validity_test()` | Path starts/ends correctly, edges exist, weight accurate | ✅ |
| 5 | No-path detection | `dijkstra_no_path_confirmed_by_bfs_test()` | Dijkstra None confirmed by BFS unreachability | ✅ |
| 6 | Undirected path symmetry | `undirected_path_symmetry_test()` | Path weight A→B equals B→A in undirected graphs | ✅ |
| 7 | Triangle inequality | `triangle_inequality_test()` | Direct path ≤ path via intermediate node | ✅ |

### Complex Invariants

| # | Property | Test Function | Rationale | Status |
|---|----------|---------------|-----------|--------|
| 8 | SCC components partition graph | `scc_partition_test()` | Components are disjoint and cover all nodes | ✅ |
| 9 | MST is spanning tree | `mst_spanning_tree_test()` | MST reaches all nodes in connected graph | ✅ |
| 10 | Bridge removal disconnects graph | `bridge_removal_test()` | Removing bridge increases connected components | ✅ |
| 11 | Degree centrality correctness | `degree_centrality_correctness_test()` | Normalized degree values match expectations | ✅ |
| 12 | Betweenness centrality non-negative | `betweenness_centrality_non_negative_test()` | All betweenness scores ≥ 0 | ✅ |
| 13 | Closeness centrality range | `closeness_centrality_in_valid_range_test()` | All closeness scores in [0, 1] | ✅ |

## Testing Strategy

### Hybrid Approach

Two complementary strategies are used:

**1. Property-Based Testing (PBT)**

- Random graph generation via `qcheck`
- ~100 test cases per property
- Graph sizes: 0-15 nodes, 0-30 edges
- Best for: Structural properties, transformations

**2. Example-Based Testing**

- Specific graph configurations
- Deterministic, fast execution
- Best for: Complex algorithms, performance-sensitive operations

### Graph Generators

```gleam
graph_generator() // Random directed/undirected
undirected_graph_generator() // Random undirected
directed_graph_generator() // Random directed
graph_generator_custom(kind, n, e) // Custom size
```

## Known Issues

### Undirected Edge Removal Asymmetry

**Status:** Documented, planned fix in v4.0

**Behavior (v3.x):**

```gleam
graph
|> model.add_edge(0, 1, 10) // Adds BOTH 0→1 and 1→0
|> model.remove_edge(0, 1) // Removes ONLY 0→1
```

**Workaround:**

```gleam
graph
|> model.remove_edge(0, 1)
|> model.remove_edge(1, 0) // Must remove both directions
```

**Reference:** `model.gleam` lines 293-295, 324-328

## Implementation Details

### Dependencies

```toml
[dev-dependencies]
qcheck = ">= 1.0.0 and < 2.0.0"
```

### Configuration

- Test framework: `gleeunit`
- Property library: `qcheck` v1.0.4
- Test timeout: 120s default
- Shrinking: Automatic (qcheck)

## References

- qcheck documentation: https://hexdocs.pm/qcheck/
- QuickCheck paper: https://www.cs.tufts.edu/~nr/cs257/archive/john-hughes/quick.pdf
- Property-Based Testing: https://hypothesis.works/articles/what-is-property-based-testing/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ In the world of computer science, this is the literal definition of Graph Theory
- **Minimum Cost Flow (MCF)**: Global optimization using the robust **Network Simplex** algorithm
- **Disjoint Set (Union-Find)**: With path compression and union by rank
- **Efficient Data Structures**: Pairing heap for priority queues, two-list queue for BFS
- **Property-Based Testing**: Exhaustively tested across core graph operations and invariants using `qcheck`

## Installation

Expand Down
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ gleamy_bench = ">= 0.6.0 and < 1.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
qcheck = ">= 1.0.0 and < 2.0.0"

[javascript]
typescript_declarations = true
6 changes: 6 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
# You typically do not need to edit this file

packages = [
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
{ name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "gleamy_bench", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleamy_bench", source = "hex", outer_checksum = "DEF68E4B097A56781282F0F9D48371A0ABBCDDCF89CAD05B28C3BEDD6B2E8DF3" },
{ name = "gleamy_structures", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleamy_structures", source = "hex", outer_checksum = "0FDAD728207FBD780CCB37EE52A25C1A5497B650F88E0ED589BEF76D8DE9AE47" },
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
{ name = "prng", version = "5.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "prng", source = "hex", outer_checksum = "29DA88BCFB54D06DD1472951DF101E9524878056D139DA2616B04350B403CE10" },
{ name = "qcheck", version = "1.0.4", build_tools = ["gleam"], requirements = ["exception", "gleam_regexp", "gleam_stdlib", "gleam_yielder", "prng"], otp_app = "qcheck", source = "hex", outer_checksum = "BD9B18C1602FBC667E3E139ED270458F8C743ED791F892CC90989CE10478C4FA" },
]

[requirements]
Expand All @@ -15,3 +20,4 @@ gleam_stdlib = { version = ">= 0.69.0 and < 1.0.0" }
gleamy_bench = { version = ">= 0.6.0 and < 1.0.0" }
gleamy_structures = { version = ">= 1.2.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
qcheck = { version = ">= 1.0.0 and < 2.0.0" }
Loading
Loading