Initial commit
This commit is contained in:
1
tests/unit/__init__.py
Normal file
1
tests/unit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Unit tests."""
|
||||
141
tests/unit/test_analysis.py
Normal file
141
tests/unit/test_analysis.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Tests for analysis and benchmarking tools."""
|
||||
|
||||
import pytest
|
||||
from src.analysis.analyzer import MazeAnalyzer
|
||||
from src.analysis.benchmark import Benchmark
|
||||
|
||||
|
||||
class TestMazeAnalyzer:
|
||||
"""Test maze analysis functionality."""
|
||||
|
||||
def test_analyze_returns_complete_data(self, medium_maze):
|
||||
"""Test that analyze returns all required fields."""
|
||||
result = MazeAnalyzer.analyze(medium_maze)
|
||||
|
||||
required_fields = [
|
||||
'dimensions', 'total_cells', 'algorithm', 'generation_time_ms',
|
||||
'seed', 'dead_ends', 'dead_end_percentage', 'longest_path_length',
|
||||
'longest_path_start', 'longest_path_end', 'average_branching_factor'
|
||||
]
|
||||
|
||||
for field in required_fields:
|
||||
assert field in result
|
||||
|
||||
def test_dead_ends_count(self, small_maze):
|
||||
"""Test dead ends counting."""
|
||||
dead_ends = MazeAnalyzer.count_dead_ends(small_maze)
|
||||
|
||||
assert dead_ends >= 0
|
||||
assert dead_ends <= small_maze.rows * small_maze.cols
|
||||
|
||||
def test_dead_end_percentage(self, medium_maze):
|
||||
"""Test dead end percentage calculation."""
|
||||
result = MazeAnalyzer.analyze(medium_maze)
|
||||
|
||||
assert 0 <= result['dead_end_percentage'] <= 100
|
||||
|
||||
def test_longest_path(self, small_maze):
|
||||
"""Test longest path finding."""
|
||||
result = MazeAnalyzer.find_longest_path(small_maze)
|
||||
|
||||
assert 'length' in result
|
||||
assert 'start' in result
|
||||
assert 'end' in result
|
||||
assert result['length'] >= 0
|
||||
|
||||
def test_branching_factor(self, medium_maze):
|
||||
"""Test branching factor calculation."""
|
||||
branching_factor = MazeAnalyzer.calculate_branching_factor(medium_maze)
|
||||
|
||||
# Branching factor should be between 1 and 4
|
||||
assert 1.0 <= branching_factor <= 4.0
|
||||
|
||||
def test_total_cells(self, medium_maze):
|
||||
"""Test total cells calculation."""
|
||||
result = MazeAnalyzer.analyze(medium_maze)
|
||||
|
||||
assert result['total_cells'] == medium_maze.rows * medium_maze.cols
|
||||
|
||||
|
||||
class TestBenchmark:
|
||||
"""Test benchmarking functionality."""
|
||||
|
||||
def test_benchmark_generators_runs(self):
|
||||
"""Test that generator benchmark runs successfully."""
|
||||
result = Benchmark.benchmark_generators(
|
||||
sizes=[(5, 5), (10, 10)],
|
||||
iterations=2,
|
||||
seed=42
|
||||
)
|
||||
|
||||
assert 'benchmark_type' in result
|
||||
assert result['benchmark_type'] == 'generators'
|
||||
assert 'results' in result
|
||||
assert len(result['results']) > 0
|
||||
|
||||
def test_benchmark_solvers_runs(self):
|
||||
"""Test that solver benchmark runs successfully."""
|
||||
result = Benchmark.benchmark_solvers(
|
||||
sizes=[(5, 5), (10, 10)],
|
||||
iterations=2,
|
||||
seed=42
|
||||
)
|
||||
|
||||
assert 'benchmark_type' in result
|
||||
assert result['benchmark_type'] == 'solvers'
|
||||
assert 'results' in result
|
||||
assert len(result['results']) > 0
|
||||
|
||||
def test_quick_benchmark(self):
|
||||
"""Test quick benchmark runs."""
|
||||
result = Benchmark.quick_benchmark()
|
||||
|
||||
assert 'generators' in result
|
||||
assert 'solvers' in result
|
||||
|
||||
def test_benchmark_generator_results_structure(self):
|
||||
"""Test benchmark generator results have correct structure."""
|
||||
result = Benchmark.benchmark_generators(
|
||||
sizes=[(5, 5)],
|
||||
iterations=2,
|
||||
seed=42
|
||||
)
|
||||
|
||||
for r in result['results']:
|
||||
assert 'algorithm' in r
|
||||
assert 'size' in r
|
||||
assert 'avg_time_ms' in r
|
||||
assert 'min_time_ms' in r
|
||||
assert 'max_time_ms' in r
|
||||
assert r['avg_time_ms'] >= 0
|
||||
|
||||
def test_benchmark_solver_results_structure(self):
|
||||
"""Test benchmark solver results have correct structure."""
|
||||
result = Benchmark.benchmark_solvers(
|
||||
sizes=[(5, 5)],
|
||||
iterations=2,
|
||||
seed=42
|
||||
)
|
||||
|
||||
for r in result['results']:
|
||||
assert 'algorithm' in r
|
||||
assert 'size' in r
|
||||
assert 'avg_time_ms' in r
|
||||
assert 'avg_path_length' in r
|
||||
assert r['avg_time_ms'] >= 0
|
||||
assert r['avg_path_length'] > 0
|
||||
|
||||
def test_benchmark_multiple_sizes(self):
|
||||
"""Test benchmark with multiple sizes."""
|
||||
sizes = [(5, 5), (10, 10)]
|
||||
result = Benchmark.benchmark_generators(
|
||||
sizes=sizes,
|
||||
iterations=2,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Should have results for each algorithm at each size
|
||||
num_algorithms = len(Benchmark.GENERATORS)
|
||||
expected_results = num_algorithms * len(sizes)
|
||||
|
||||
assert len(result['results']) == expected_results
|
||||
148
tests/unit/test_generators.py
Normal file
148
tests/unit/test_generators.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Tests for maze generation algorithms."""
|
||||
|
||||
import pytest
|
||||
from src.generators import (
|
||||
RecursiveBacktrackingGenerator,
|
||||
KruskalGenerator,
|
||||
PrimGenerator,
|
||||
SidewinderGenerator,
|
||||
HuntAndKillGenerator,
|
||||
EllerGenerator,
|
||||
WilsonGenerator,
|
||||
AldousBroderGenerator
|
||||
)
|
||||
|
||||
|
||||
# All generators to test
|
||||
GENERATORS = [
|
||||
RecursiveBacktrackingGenerator(),
|
||||
KruskalGenerator(),
|
||||
PrimGenerator(),
|
||||
SidewinderGenerator(),
|
||||
HuntAndKillGenerator(),
|
||||
EllerGenerator(),
|
||||
WilsonGenerator(),
|
||||
AldousBroderGenerator()
|
||||
]
|
||||
|
||||
|
||||
class TestGenerators:
|
||||
"""Test all maze generation algorithms."""
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_generator_creates_valid_maze(self, generator):
|
||||
"""Test that generator creates a valid maze."""
|
||||
maze = generator.generate(10, 10, seed=42)
|
||||
|
||||
assert maze is not None
|
||||
assert maze.rows == 10
|
||||
assert maze.cols == 10
|
||||
assert maze.algorithm_used == generator.name
|
||||
assert maze.generation_time_ms >= 0
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_generator_with_different_sizes(self, generator):
|
||||
"""Test generator with different maze sizes."""
|
||||
# Small maze
|
||||
maze_small = generator.generate(5, 5, seed=42)
|
||||
assert maze_small.rows == 5
|
||||
assert maze_small.cols == 5
|
||||
|
||||
# Large maze
|
||||
maze_large = generator.generate(25, 25, seed=42)
|
||||
assert maze_large.rows == 25
|
||||
assert maze_large.cols == 25
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_generator_reproducibility(self, generator):
|
||||
"""Test that same seed produces same maze."""
|
||||
maze1 = generator.generate(10, 10, seed=42)
|
||||
maze2 = generator.generate(10, 10, seed=42)
|
||||
|
||||
# Compare wall structures
|
||||
for row in range(10):
|
||||
for col in range(10):
|
||||
cell1 = maze1.get_cell(row, col)
|
||||
cell2 = maze2.get_cell(row, col)
|
||||
assert cell1.walls == cell2.walls
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_maze_is_fully_connected(self, generator):
|
||||
"""Test that all cells in maze are reachable."""
|
||||
maze = generator.generate(10, 10, seed=42)
|
||||
|
||||
# Use BFS to check connectivity
|
||||
start = maze.get_cell(0, 0)
|
||||
visited = set()
|
||||
queue = [start]
|
||||
start.visited = True
|
||||
|
||||
while queue:
|
||||
current = queue.pop(0)
|
||||
visited.add((current.row, current.col))
|
||||
|
||||
neighbors = maze.get_neighbors(current)
|
||||
for neighbor, direction in neighbors:
|
||||
if not neighbor.visited and not current.has_wall(direction):
|
||||
neighbor.visited = True
|
||||
queue.append(neighbor)
|
||||
|
||||
# All cells should be reachable
|
||||
assert len(visited) == maze.rows * maze.cols
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_maze_has_passages(self, generator):
|
||||
"""Test that maze has passages (some walls removed)."""
|
||||
maze = generator.generate(10, 10, seed=42)
|
||||
|
||||
total_walls = 0
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
total_walls += sum(1 for wall in cell.walls.values() if wall)
|
||||
|
||||
# Should have fewer walls than a completely walled maze
|
||||
max_walls = 10 * 10 * 4
|
||||
assert total_walls < max_walls
|
||||
|
||||
@pytest.mark.parametrize("generator", GENERATORS)
|
||||
def test_generator_performance(self, generator):
|
||||
"""Test generator meets performance targets."""
|
||||
# 10x10 should be very fast
|
||||
maze = generator.generate(10, 10, seed=42)
|
||||
assert maze.generation_time_ms < 1000 # Less than 1 second
|
||||
|
||||
# Even 25x25 should be reasonable (except Aldous-Broder can be slow)
|
||||
if generator.name != "Aldous-Broder Algorithm":
|
||||
maze = generator.generate(25, 25, seed=42)
|
||||
assert maze.generation_time_ms < 5000 # Less than 5 seconds
|
||||
|
||||
|
||||
class TestSpecificGenerators:
|
||||
"""Test specific generator properties."""
|
||||
|
||||
def test_recursive_backtracking_name(self):
|
||||
"""Test recursive backtracking has correct name."""
|
||||
gen = RecursiveBacktrackingGenerator()
|
||||
assert gen.name == "Recursive Backtracking"
|
||||
|
||||
def test_kruskal_name(self):
|
||||
"""Test Kruskal's has correct name."""
|
||||
gen = KruskalGenerator()
|
||||
assert gen.name == "Kruskal's Algorithm"
|
||||
|
||||
def test_prim_name(self):
|
||||
"""Test Prim's has correct name."""
|
||||
gen = PrimGenerator()
|
||||
assert gen.name == "Prim's Algorithm"
|
||||
|
||||
def test_sidewinder_creates_valid_maze(self):
|
||||
"""Test Sidewinder algorithm."""
|
||||
gen = SidewinderGenerator()
|
||||
maze = gen.generate(10, 10, seed=42)
|
||||
|
||||
# Top row should have all east walls removed (characteristic of Sidewinder)
|
||||
# Check that top row is mostly connected horizontally
|
||||
top_row = maze.grid[0]
|
||||
east_walls = sum(1 for cell in top_row if cell.has_wall('east'))
|
||||
# Should have mostly removed east walls in top row
|
||||
assert east_walls < len(top_row)
|
||||
173
tests/unit/test_maze.py
Normal file
173
tests/unit/test_maze.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Tests for core Maze and Cell classes."""
|
||||
|
||||
import pytest
|
||||
from src.core.maze import Maze
|
||||
from src.core.cell import Cell
|
||||
|
||||
|
||||
class TestCell:
|
||||
"""Test Cell class functionality."""
|
||||
|
||||
def test_cell_initialization(self):
|
||||
"""Test cell is initialized with all walls."""
|
||||
cell = Cell(0, 0)
|
||||
assert cell.row == 0
|
||||
assert cell.col == 0
|
||||
assert all(cell.walls.values())
|
||||
assert not cell.visited
|
||||
|
||||
def test_remove_wall(self):
|
||||
"""Test removing walls from a cell."""
|
||||
cell = Cell(0, 0)
|
||||
cell.remove_wall('north')
|
||||
assert not cell.has_wall('north')
|
||||
assert cell.has_wall('south')
|
||||
|
||||
def test_cell_reset(self):
|
||||
"""Test resetting a cell."""
|
||||
cell = Cell(0, 0)
|
||||
cell.remove_wall('north')
|
||||
cell.visited = True
|
||||
cell.reset()
|
||||
assert cell.has_wall('north')
|
||||
assert not cell.visited
|
||||
|
||||
def test_cell_serialization(self):
|
||||
"""Test cell to_dict and from_dict."""
|
||||
cell = Cell(2, 3)
|
||||
cell.remove_wall('east')
|
||||
cell.visited = True
|
||||
|
||||
data = cell.to_dict()
|
||||
restored = Cell.from_dict(data)
|
||||
|
||||
assert restored.row == cell.row
|
||||
assert restored.col == cell.col
|
||||
assert restored.walls == cell.walls
|
||||
assert restored.visited == cell.visited
|
||||
|
||||
def test_cell_equality(self):
|
||||
"""Test cell equality."""
|
||||
cell1 = Cell(0, 0)
|
||||
cell2 = Cell(0, 0)
|
||||
cell3 = Cell(1, 1)
|
||||
|
||||
assert cell1 == cell2
|
||||
assert cell1 != cell3
|
||||
|
||||
|
||||
class TestMaze:
|
||||
"""Test Maze class functionality."""
|
||||
|
||||
def test_maze_initialization(self):
|
||||
"""Test maze is initialized correctly."""
|
||||
maze = Maze(10, 10, seed=42)
|
||||
assert maze.rows == 10
|
||||
assert maze.cols == 10
|
||||
assert maze.seed == 42
|
||||
assert len(maze.grid) == 10
|
||||
assert len(maze.grid[0]) == 10
|
||||
|
||||
def test_maze_dimensions_validation(self):
|
||||
"""Test maze dimension validation."""
|
||||
with pytest.raises(ValueError):
|
||||
Maze(3, 10) # Too small
|
||||
with pytest.raises(ValueError):
|
||||
Maze(10, 60) # Too large
|
||||
|
||||
def test_get_cell(self):
|
||||
"""Test getting cells from maze."""
|
||||
maze = Maze(10, 10)
|
||||
cell = maze.get_cell(5, 5)
|
||||
assert cell is not None
|
||||
assert cell.row == 5
|
||||
assert cell.col == 5
|
||||
|
||||
# Out of bounds
|
||||
assert maze.get_cell(-1, 0) is None
|
||||
assert maze.get_cell(0, 100) is None
|
||||
|
||||
def test_get_neighbors(self):
|
||||
"""Test getting neighbors of a cell."""
|
||||
maze = Maze(10, 10)
|
||||
|
||||
# Corner cell
|
||||
cell = maze.get_cell(0, 0)
|
||||
neighbors = maze.get_neighbors(cell)
|
||||
assert len(neighbors) == 2 # Only south and east
|
||||
|
||||
# Middle cell
|
||||
cell = maze.get_cell(5, 5)
|
||||
neighbors = maze.get_neighbors(cell)
|
||||
assert len(neighbors) == 4 # All directions
|
||||
|
||||
def test_remove_wall_between(self):
|
||||
"""Test removing walls between cells."""
|
||||
maze = Maze(10, 10)
|
||||
cell1 = maze.get_cell(0, 0)
|
||||
cell2 = maze.get_cell(0, 1)
|
||||
|
||||
maze.remove_wall_between(cell1, cell2)
|
||||
|
||||
assert not cell1.has_wall('east')
|
||||
assert not cell2.has_wall('west')
|
||||
|
||||
def test_reset_visited(self):
|
||||
"""Test resetting visited flags."""
|
||||
maze = Maze(10, 10)
|
||||
|
||||
# Mark some cells as visited
|
||||
for row in maze.grid[:5]:
|
||||
for cell in row:
|
||||
cell.visited = True
|
||||
|
||||
maze.reset_visited()
|
||||
|
||||
# Check all cells are unvisited
|
||||
for row in maze.grid:
|
||||
for cell in row:
|
||||
assert not cell.visited
|
||||
|
||||
def test_maze_serialization(self):
|
||||
"""Test maze to_dict and from_dict."""
|
||||
maze = Maze(5, 5, seed=42)
|
||||
maze.algorithm_used = "Test Algorithm"
|
||||
maze.generation_time_ms = 10.5
|
||||
|
||||
# Modify some walls
|
||||
cell1 = maze.get_cell(0, 0)
|
||||
cell2 = maze.get_cell(0, 1)
|
||||
maze.remove_wall_between(cell1, cell2)
|
||||
|
||||
# Serialize and deserialize
|
||||
data = maze.to_dict()
|
||||
restored = Maze.from_dict(data)
|
||||
|
||||
assert restored.rows == maze.rows
|
||||
assert restored.cols == maze.cols
|
||||
assert restored.seed == maze.seed
|
||||
assert restored.algorithm_used == maze.algorithm_used
|
||||
|
||||
# Check walls are preserved
|
||||
restored_cell1 = restored.get_cell(0, 0)
|
||||
assert not restored_cell1.has_wall('east')
|
||||
|
||||
def test_maze_json(self):
|
||||
"""Test JSON serialization."""
|
||||
maze = Maze(5, 5, seed=42)
|
||||
json_str = maze.to_json()
|
||||
|
||||
restored = Maze.from_json(json_str)
|
||||
|
||||
assert restored.rows == maze.rows
|
||||
assert restored.cols == maze.cols
|
||||
assert restored.seed == maze.seed
|
||||
|
||||
def test_is_valid_position(self):
|
||||
"""Test position validation."""
|
||||
maze = Maze(10, 10)
|
||||
|
||||
assert maze.is_valid_position(0, 0)
|
||||
assert maze.is_valid_position(9, 9)
|
||||
assert not maze.is_valid_position(-1, 0)
|
||||
assert not maze.is_valid_position(0, 10)
|
||||
143
tests/unit/test_solvers.py
Normal file
143
tests/unit/test_solvers.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Tests for maze solving algorithms."""
|
||||
|
||||
import pytest
|
||||
from src.solvers import DFSSolver, BFSSolver
|
||||
from src.generators import RecursiveBacktrackingGenerator
|
||||
|
||||
|
||||
SOLVERS = [DFSSolver(), BFSSolver()]
|
||||
|
||||
|
||||
class TestSolvers:
|
||||
"""Test maze solving algorithms."""
|
||||
|
||||
@pytest.mark.parametrize("solver", SOLVERS)
|
||||
def test_solver_finds_solution(self, solver, small_maze):
|
||||
"""Test that solver finds a solution."""
|
||||
result = solver.solve(small_maze)
|
||||
|
||||
assert result['success']
|
||||
assert result['path'] is not None
|
||||
assert len(result['path']) > 0
|
||||
assert result['path_length'] > 0
|
||||
assert result['time_ms'] >= 0
|
||||
|
||||
@pytest.mark.parametrize("solver", SOLVERS)
|
||||
def test_solution_path_validity(self, solver, medium_maze):
|
||||
"""Test that solution path is valid."""
|
||||
result = solver.solve(medium_maze)
|
||||
|
||||
assert result['success']
|
||||
path = result['path']
|
||||
|
||||
# Path should start at maze start
|
||||
assert path[0] == medium_maze.start
|
||||
|
||||
# Path should end at maze end
|
||||
assert path[-1] == medium_maze.end
|
||||
|
||||
# Each step should be adjacent to previous
|
||||
for i in range(len(path) - 1):
|
||||
r1, c1 = path[i]
|
||||
r2, c2 = path[i + 1]
|
||||
|
||||
# Manhattan distance should be 1
|
||||
assert abs(r2 - r1) + abs(c2 - c1) == 1
|
||||
|
||||
@pytest.mark.parametrize("solver", SOLVERS)
|
||||
def test_solver_visited_cells(self, solver, small_maze):
|
||||
"""Test that solver tracks visited cells."""
|
||||
result = solver.solve(small_maze)
|
||||
|
||||
assert 'visited' in result
|
||||
assert len(result['visited']) > 0
|
||||
|
||||
# Solution path should be subset of visited cells
|
||||
path_set = set(result['path'])
|
||||
visited_set = set(result['visited'])
|
||||
assert path_set.issubset(visited_set)
|
||||
|
||||
def test_bfs_finds_shortest_path(self):
|
||||
"""Test that BFS finds shortest path."""
|
||||
gen = RecursiveBacktrackingGenerator()
|
||||
maze = gen.generate(10, 10, seed=42)
|
||||
|
||||
bfs = BFSSolver()
|
||||
dfs = DFSSolver()
|
||||
|
||||
bfs_result = bfs.solve(maze)
|
||||
dfs_result = dfs.solve(maze)
|
||||
|
||||
# BFS should find shortest or equal path
|
||||
assert bfs_result['path_length'] <= dfs_result['path_length']
|
||||
|
||||
def test_solver_performance(self):
|
||||
"""Test solver performance."""
|
||||
gen = RecursiveBacktrackingGenerator()
|
||||
maze = gen.generate(25, 25, seed=42)
|
||||
|
||||
for solver in SOLVERS:
|
||||
result = solver.solve(maze)
|
||||
# Should solve 25x25 maze quickly
|
||||
assert result['time_ms'] < 1000
|
||||
|
||||
def test_solver_on_different_sizes(self):
|
||||
"""Test solvers on different maze sizes."""
|
||||
gen = RecursiveBacktrackingGenerator()
|
||||
|
||||
for size in [5, 10, 15, 20]:
|
||||
maze = gen.generate(size, size, seed=42)
|
||||
|
||||
for solver in SOLVERS:
|
||||
result = solver.solve(maze)
|
||||
assert result['success']
|
||||
assert result['path_length'] > 0
|
||||
|
||||
|
||||
class TestDFSSolver:
|
||||
"""Test DFS-specific functionality."""
|
||||
|
||||
def test_dfs_name(self):
|
||||
"""Test DFS solver name."""
|
||||
solver = DFSSolver()
|
||||
assert "DFS" in solver.name or "Depth-First" in solver.name
|
||||
|
||||
def test_dfs_solves_maze(self, medium_maze):
|
||||
"""Test DFS solves maze correctly."""
|
||||
solver = DFSSolver()
|
||||
result = solver.solve(medium_maze)
|
||||
|
||||
assert result['success']
|
||||
assert result['algorithm'] == solver.name
|
||||
|
||||
|
||||
class TestBFSSolver:
|
||||
"""Test BFS-specific functionality."""
|
||||
|
||||
def test_bfs_name(self):
|
||||
"""Test BFS solver name."""
|
||||
solver = BFSSolver()
|
||||
assert "BFS" in solver.name or "Breadth-First" in solver.name
|
||||
|
||||
def test_bfs_solves_maze(self, medium_maze):
|
||||
"""Test BFS solves maze correctly."""
|
||||
solver = BFSSolver()
|
||||
result = solver.solve(medium_maze)
|
||||
|
||||
assert result['success']
|
||||
assert result['algorithm'] == solver.name
|
||||
|
||||
def test_bfs_optimal_path(self):
|
||||
"""Test BFS finds optimal path."""
|
||||
gen = RecursiveBacktrackingGenerator()
|
||||
|
||||
# Test on multiple mazes
|
||||
for seed in [42, 100, 200]:
|
||||
maze = gen.generate(15, 15, seed=seed)
|
||||
|
||||
bfs = BFSSolver()
|
||||
result = bfs.solve(maze)
|
||||
|
||||
# Verify path exists and is valid
|
||||
assert result['success']
|
||||
assert result['path_length'] > 0
|
||||
Reference in New Issue
Block a user