"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos absolutos."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]


def run(*command):
    return subprocess.run(
        list(command), cwd=HERE, text=True, stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )


class Examples(unittest.TestCase):
    def test_list_tests_pass(self):
        done = run("build/test_structures")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_programs_exit_cleanly(self):
        for name in ["slist_demo", "dlist_demo", "trace_range", "invalidation", "erase_evens",
                     "same_interface", "alloc_count", "node_addresses"]:
            with self.subTest(program=name):
                done = run(f"build/{name}")
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_allocations_are_counted(self):
        lines = dict(line.split(": ") for line in run("build/alloc_count").stdout.splitlines()
                     if ": " in line)
        self.assertEqual(int(lines["DList"]), 1000001)
        self.assertEqual(int(lines["std::list"]), 1000000)
        self.assertLess(int(lines["std::vector"]), 40)

    def test_shuffle_really_scatters_the_nodes(self):
        """Los porcentajes de nodos seguidos son conteos, y las sumas deben coincidir."""
        rows = run("build/traversal_bench").stdout.splitlines()[1:]
        self.assertEqual(len(rows), 7)
        for row in rows:
            fields = row.split()
            with self.subTest(n=fields[0]):
                self.assertEqual(fields[-1], "ok")
                self.assertGreater(float(fields[-3].rstrip("%")), 95.0)
                self.assertLess(float(fields[-2].rstrip("%")), 5.0)


class DeliberateErrors(unittest.TestCase):
    def build(self, name):
        binary = f"build/{name}"
        done = run(*SAN, f"errors/{name}.cpp", "-o", binary)
        self.assertEqual(done.returncode, 0, done.stdout)
        return run(binary)

    def test_use_after_delete(self):
        done = self.build("use_after_delete")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("heap-use-after-free", done.stdout)
        self.assertIn("errors/use_after_delete.cpp:14", done.stdout)

    def test_erase_in_range_for(self):
        done = self.build("erase_in_range_for")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("heap-use-after-free", done.stdout)
        self.assertRegex(done.stdout, re.compile(r"operator\+\+"))

    def test_stale_vector_iterator(self):
        done = self.build("stale_vector_iterator")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("heap-use-after-free", done.stdout)

    def test_forgot_prev_is_found_by_invariants(self):
        done = self.build("forgot_prev")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("1 2", done.stdout)
        self.assertIn("check_invariants", done.stdout)
        self.assertNotIn("AddressSanitizer", done.stdout)


if __name__ == "__main__":
    unittest.main()
