diff --git a/app/src/main/kotlin/model/algo/findMST.kt b/app/src/main/kotlin/model/algo/findMST.kt index 5defd11..55dee0b 100644 --- a/app/src/main/kotlin/model/algo/findMST.kt +++ b/app/src/main/kotlin/model/algo/findMST.kt @@ -6,8 +6,9 @@ import model.graph.Edge fun findMST(graph: Graph): Set { require(!graph.isDirected) { "MST is only implemented for undirected graphs" } - val sortedEdges = graph.edges.sortedBy { it.weight } val mstEdges = mutableSetOf() + if (graph.vertices.isEmpty()) return mstEdges + val sortedEdges = graph.edges.sortedBy { it.weight } val verticesList = graph.vertices.toList() val idToIndex = verticesList diff --git a/app/src/test/kotlin/algorithms/CommunitiesTest.kt b/app/src/test/kotlin/algorithms/CommunitiesTest.kt new file mode 100644 index 0000000..698f80d --- /dev/null +++ b/app/src/test/kotlin/algorithms/CommunitiesTest.kt @@ -0,0 +1,91 @@ +package algorithms + +import model.algo.findCommunities +import model.graph.Graph +import model.graph.Vertex +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class CommunitiesTest { + /** + * Test checks whether graph is complete there is certainly one community + */ + @Test + fun `one community, complete graph`() { + val graph = Graph() + graph.addVertex("0") + graph.addVertex("1") + graph.addVertex("2") + graph.addVertex("3") + + graph.addEdge(1,0) + graph.addEdge(1,2) + graph.addEdge(1,3) + graph.addEdge(0,2) + graph.addEdge(0,3) + graph.addEdge(2,3) + + assertEquals(1, findCommunities(graph).size) + } + + /** + * Each vertex from 5000 is its own community + */ + @Test + fun `5000 communities, no edges`() { + val graph = Graph() + for (i in 1..5000) { + graph.addVertex(i.toString()) + } + val result = findCommunities(graph).size + assertEquals( 5000, result) + } + + /** + * No graph - no communities + */ + @Test + fun `empty graph, no communities`() { + val graph = Graph() + assertTrue(findCommunities(graph).isEmpty()) + } + + /** + * three vertices connected with each other + */ + @Test + fun `two communities connected with one edge`() { + val graph = Graph() + val firstComm = mutableSetOf() + val secondComm = mutableSetOf() + + firstComm.add(graph.addVertex("0")) + firstComm.add(graph.addVertex("1")) + firstComm.add(graph.addVertex("2")) + secondComm.add(graph.addVertex("3")) + secondComm.add(graph.addVertex("4")) + secondComm.add(graph.addVertex("5")) + + graph.addEdge(1,0) + graph.addEdge(0,2) + graph.addEdge(2,1) + graph.addEdge(3,4) + graph.addEdge(4,5) + graph.addEdge(5,3) + graph.addEdge(0,3) + + assertEquals(firstComm, findCommunities(graph).getValue(0).toSet()) + assertEquals(secondComm, findCommunities(graph).getValue(1).toSet()) + } + + /** + * One vertex - its own community + */ + @Test + fun `single isolated vertex is its own community`() { + val graph = Graph() + graph.addVertex("0") + assertEquals(1, findCommunities(graph).size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/algorithms/MstTest.kt b/app/src/test/kotlin/algorithms/MstTest.kt new file mode 100644 index 0000000..8a48c74 --- /dev/null +++ b/app/src/test/kotlin/algorithms/MstTest.kt @@ -0,0 +1,96 @@ +package algorithms + +import model.algo.findMST +import model.graph.Edge +import model.graph.Graph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class MstTest { + /** + * Chain produce one mst + */ + @Test + fun `all edges in mst`() { + val graph = Graph() + val expected = mutableSetOf() + graph.addVertex("0") + graph.addVertex("1") + graph.addVertex("2") + graph.addVertex("3") + + expected.add(graph.addEdge(0,1)) + expected.add(graph.addEdge(1,2)) + expected.add(graph.addEdge(2,3)) + + val result = findMST(graph) + assertEquals(expected, result) + } + + /** + * If there is no edges there is no mst + */ + @Test + fun `graph is not connected`() { + val graph = Graph() + graph.addVertex("0") + graph.addVertex("1") + graph.addVertex("2") + graph.addVertex("3") + + assertThrows { + findMST(graph) + } + } + + /** + * Mst cannot be find in directed graphs + */ + @Test + fun `directed graph throws exception`() { + val graph = Graph(true) + assertThrows { + findMST(graph) + } + } + + /** + * No edges - no mst + */ + @Test + fun `single vertex returns empty mst`() { + val graph = Graph() + graph.addVertex("0") + assertTrue(findMST(graph).isEmpty()) + } + + /** + * Test checks if negative weight is supported + */ + @Test + fun `negative weight is supported`() { + val graph = Graph() + val expected = mutableSetOf() + + graph.addVertex("0") + graph.addVertex("1") + graph.addVertex("2") + + expected.add(graph.addEdge(0, 1, -5)) + expected.add(graph.addEdge(1, 2, 2)) + + val result = findMST(graph) + assertEquals(expected, result) + } + + /** + * If there is no graph - mst is empty + */ + @Test + fun `empty graph - empty mst`() { + val graph = Graph() + assertTrue(findMST(graph).isEmpty()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/algorithms/SccTest.kt b/app/src/test/kotlin/algorithms/SccTest.kt new file mode 100644 index 0000000..d4be064 --- /dev/null +++ b/app/src/test/kotlin/algorithms/SccTest.kt @@ -0,0 +1,80 @@ +package algorithms + +import model.algo.findSCC +import model.graph.Graph +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class SccTest { + /** + * No graph - no scc + */ + @Test + fun `empty graph no scc`() { + assertTrue(findSCC(Graph()).isEmpty()) + } + + /** + * Single vertex is strongly connected component + */ + @Test + fun `single vertex graph`() { + val graph = Graph() + graph.addVertex("0") + val scc = findSCC(graph) + assertEquals(1, scc.size) + assertEquals(1, scc.first().size) + } + + /** + * Each vertex is scc because there is no edges + */ + @Test + fun `5000 scc, no edges`() { + val graph = Graph() + for (i in 1..5000) { + graph.addVertex(i.toString()) + } + val result = findSCC(graph).size + assertEquals( 5000, result) + } + + /** + * Cycle produces one scc + */ + @Test + fun `directed cycle forms one scc`() { + val graph = Graph(true) + for (i in 0 until 100) { + graph.addVertex(i.toString()) + } + + for (i in 0 until 100) { + val from = i + val to = (i + 1) % 100 + graph.addEdge(from, to) + } + val scc = findSCC(graph) + assertEquals(1, scc.size) + assertEquals(100, scc.first().size) + } + + /** + * Each vertex is reachable - scc + */ + @Test + fun `chain in directed graph produces 100 scc`() { + val graph = Graph(true) + for (i in 0 until 100) { + graph.addVertex(i.toString()) + } + + for (i in 0 until 99) { + graph.addEdge(i, i + 1) + } + val scc = findSCC(graph) + assertEquals(100, scc.size) + assertEquals(1, scc.first().size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/integration/SQLiteMstIntegrationTest.kt b/app/src/test/kotlin/integration/SQLiteMstIntegrationTest.kt new file mode 100644 index 0000000..8155778 --- /dev/null +++ b/app/src/test/kotlin/integration/SQLiteMstIntegrationTest.kt @@ -0,0 +1,59 @@ +package integration + +import androidx.compose.ui.graphics.Color +import model.graph.Edge +import model.graph.Graph +import model.io.sqlite.SqliteRepository +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import java.nio.file.Path +import viewmodel.representation.RepresentationStrategy +import viewmodel.screens.MainScreenViewModel +import viewmodel.representation.ForceAtlas2 + +class SQLiteMstIntegrationTest { + private lateinit var repo: SqliteRepository + private lateinit var representationStrategy: RepresentationStrategy + private lateinit var graph: Graph + + @BeforeEach + fun setup(@TempDir tempDir: Path) { + val dbFile = tempDir.resolve("test.db").toFile().absolutePath + repo = SqliteRepository(dbFile) + graph = Graph() + representationStrategy = ForceAtlas2() + } + + /** + * Test checks coloring of edges by basic scenario: + * user creates graph -> save it to sqlite -> load it form sqlite -> use find mst algorithm + */ + @Test + fun `create graph, load to sqlite, read from there and find mst`() { + val expected = mutableSetOf() + graph.addVertex("0") + graph.addVertex("1") + graph.addVertex("2") + expected.add(graph.addEdge(0, 1, -1)) + expected.add(graph.addEdge(1, 2, 1)) + graph.addEdge(2, 0, 10) + + val stored = repo.save(graph, "kirilenko_top") + val loaded = repo.read(stored) + assertEquals(3, loaded.vertices.size) + + val vm = MainScreenViewModel(loaded, representationStrategy) + vm.showMst() + + val expectedWeight = expected.sumOf { it.weight } + val greenEdges = vm.graphViewModel.edges.filter { it.color == Color.Green } + val grayEdges = vm.graphViewModel.edges.filter { it.color == Color.Gray } + val greenEdgesWeight = greenEdges.sumOf { it.origin.weight } + + assertEquals(expectedWeight, greenEdgesWeight) + assertEquals(loaded.edges.size - greenEdges.size, grayEdges.size) + } +} diff --git a/app/src/test/kotlin/io/SqliteTest.kt b/app/src/test/kotlin/io/SqliteTest.kt new file mode 100644 index 0000000..7f4c2d1 --- /dev/null +++ b/app/src/test/kotlin/io/SqliteTest.kt @@ -0,0 +1,124 @@ +package io + +import model.graph.Graph +import model.io.sqlite.SqliteRepository +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +class SqliteTest { + private lateinit var repo: SqliteRepository + + @BeforeEach + fun setup(@TempDir tempDir: Path) { + val dbFile = tempDir.resolve("test.db").toFile().absolutePath + repo = SqliteRepository(dbFile) + } + + /** + * Simple test for saving empty graph + */ + @Test + fun `save empty graph`() { + val id = repo.save(Graph(), "empty") + val restored = repo.read(id) + assertTrue(restored.vertices.isEmpty()) + } + + /** + * Round-trip with undirected graph + */ + @Test + fun `save and read undirected graph`() { + val graph = Graph() + graph.addVertex("a") + graph.addVertex("b") + graph.addEdge(0, 1) + + val id = repo.save(graph, "52") + val restored = repo.read(id) + + assertFalse(restored.isDirected) + assertEquals(2, restored.vertices.size) + assertEquals(1, restored.edges.size) + } + + /** + * Round-trip with directed graph + */ + @Test + fun `save and read directed graph`() { + val graph = Graph(true) + graph.addVertex("a") + graph.addVertex("b") + graph.addEdge(0, 1) + + val id = repo.save(graph, "42") + val restored = repo.read(id) + + assertTrue(restored.isDirected) + assertEquals(2, restored.vertices.size) + assertEquals(1, restored.edges.size) + } + + /** + * Test checks if update function delete previous graph + */ + @Test + fun `update overrides graph and name`() { + val original = Graph() + original.addVertex("a") + original.addVertex("b") + original.addEdge(0, 1, 4) + + val id = repo.save(original, "original") + + val new = Graph(true) + new.addVertex("c") + new.addVertex("d") + new.addEdge(0, 1, -1) + + repo.update(id, new, "new") + + val restored = repo.read(id) + assertTrue(restored.isDirected) + assertEquals(-1, restored.edges.first().weight) + val actualName = repo.listGraphs().single { it.first == id }.second + assertEquals("new", actualName) + } + + /** + * Deletion of whole graph. Cannot read it after delete + */ + @Test + fun `delete removes graph entirely`() { + val graph = Graph() + graph.addVertex("a") + val id = repo.save(graph, "to delete") + repo.delete(id) + + assertTrue(repo.listGraphs().none { it.first == id }) + assertThrows { repo.read(id) } + } + + /** + * Listing function filter properly and list in alphabetical order + */ + @Test + fun `listGraphs ordered alphabetically and filterable`() { + repo.save(Graph(), "b") + repo.save(Graph(), "a") + repo.save(Graph(), "abc") + + val allNames = repo.listGraphs().map { it.second } + assertEquals(listOf("a", "abc", "b"), allNames) + + val filtered = repo.listGraphs("a").map { it.second } + assertEquals(listOf("a", "abc"), filtered) + } +} \ No newline at end of file