上传文件至「tests」
This commit is contained in:
+297
-254
@@ -1,282 +1,325 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
|
||||
class EdaRouterPinsContractTest(unittest.TestCase):
|
||||
def test_loader_uses_top_level_pins_and_ignores_legacy_ports(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"pins": [
|
||||
{"name": "input_io1", "x": 1, "y": 2, "angle": 90, "width": 0.6},
|
||||
],
|
||||
"ports": [
|
||||
{"name": "legacy_port", "x": 9, "y": 9, "angle": 0},
|
||||
],
|
||||
})
|
||||
|
||||
self.assertIn("input_io1", spec.pins)
|
||||
self.assertNotIn("legacy_port", spec.pins)
|
||||
self.assertEqual(spec.pins["input_io1"].angle, 90.0)
|
||||
|
||||
def test_loader_accepts_pin_links_with_route_metadata(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell_a",
|
||||
"instances": {
|
||||
"inst_a": {"component": "mmi", "x": 0, "y": 0},
|
||||
"inst_b": {"component": "mmi", "x": 100, "y": 0},
|
||||
},
|
||||
"bundles": {
|
||||
"output_bus": {
|
||||
"links": [{
|
||||
"from": "inst_a:out",
|
||||
"to": "inst_b:in",
|
||||
"xsection": "metal_1",
|
||||
"family": "electrical",
|
||||
"width": 5,
|
||||
"radius": 20,
|
||||
"routing_type": "standard_bend",
|
||||
"points": [{"x": 0, "y": 0}, {"x": 50, "y": 0}],
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
link = spec.bundles["output_bus"].links[0]
|
||||
self.assertEqual(link.src_inst, "inst_a")
|
||||
self.assertEqual(link.src_pin, "out")
|
||||
self.assertEqual(link.dst_inst, "inst_b")
|
||||
self.assertEqual(link.dst_pin, "in")
|
||||
self.assertEqual(link.xsection, "metal_1")
|
||||
self.assertEqual(link.width, 5)
|
||||
self.assertEqual(link.points[1], {"x": 50.0, "y": 0.0})
|
||||
|
||||
def test_port_element_creates_named_io_pins_and_inside_route_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
def move(self, dx, dy, da):
|
||||
return FakePlacedPin(f"{self.name}_in", self.x + dx, self.y + dy, self.a + da)
|
||||
|
||||
class FakePin:
|
||||
created_names = []
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
self.width = width
|
||||
self.xs = xs
|
||||
self.created_names.append(name)
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
put_calls = []
|
||||
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
self.put_calls.append((self.name, x, y, a))
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"input": {
|
||||
"type": "port",
|
||||
"x": 5,
|
||||
"y": 10,
|
||||
"angle": 90,
|
||||
"width": 0.5,
|
||||
"port_number": 2,
|
||||
"pitch": 12,
|
||||
"pins": [
|
||||
{"name": "input_io1", "role": "io1"},
|
||||
{"name": "input_io2", "role": "io2"},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "input", spec.elements["input"], FakeNazca)
|
||||
|
||||
self.assertEqual(FakePin.created_names, ["input_io1", "input_io1_in", "input_io2", "input_io2_in"])
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
|
||||
class EdaRouterPinsContractTest(unittest.TestCase):
|
||||
def test_loader_uses_top_level_pins_and_ignores_legacy_ports(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"pins": [
|
||||
{"name": "input_io1", "x": 1, "y": 2, "angle": 90, "width": 0.6},
|
||||
],
|
||||
"ports": [
|
||||
{"name": "legacy_port", "x": 9, "y": 9, "angle": 0},
|
||||
],
|
||||
})
|
||||
|
||||
self.assertIn("input_io1", spec.pins)
|
||||
self.assertNotIn("legacy_port", spec.pins)
|
||||
self.assertEqual(spec.pins["input_io1"].angle, 90.0)
|
||||
|
||||
def test_loader_accepts_pin_links_with_route_metadata(self):
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell_a",
|
||||
"instances": {
|
||||
"inst_a": {"component": "mmi", "x": 0, "y": 0},
|
||||
"inst_b": {"component": "mmi", "x": 100, "y": 0},
|
||||
},
|
||||
"bundles": {
|
||||
"output_bus": {
|
||||
"links": [{
|
||||
"from": "inst_a:out",
|
||||
"to": "inst_b:in",
|
||||
"xsection": "metal_1",
|
||||
"family": "electrical",
|
||||
"width": 5,
|
||||
"radius": 20,
|
||||
"routing_type": "standard_bend",
|
||||
"points": [{"x": 0, "y": 0}, {"x": 50, "y": 0}],
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
link = spec.bundles["output_bus"].links[0]
|
||||
self.assertEqual(link.src_inst, "inst_a")
|
||||
self.assertEqual(link.src_pin, "out")
|
||||
self.assertEqual(link.dst_inst, "inst_b")
|
||||
self.assertEqual(link.dst_pin, "in")
|
||||
self.assertEqual(link.xsection, "metal_1")
|
||||
self.assertEqual(link.width, 5)
|
||||
self.assertEqual(link.points[1], {"x": 50.0, "y": 0.0})
|
||||
|
||||
def test_port_element_creates_named_io_pins_and_inside_route_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
def move(self, dx, dy, da):
|
||||
return FakePlacedPin(f"{self.name}_in", self.x + dx, self.y + dy, self.a + da)
|
||||
|
||||
class FakePin:
|
||||
created_names = []
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
self.width = width
|
||||
self.xs = xs
|
||||
self.created_names.append(name)
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
put_calls = []
|
||||
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
self.put_calls.append((self.name, x, y, a))
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"input": {
|
||||
"type": "port",
|
||||
"x": 5,
|
||||
"y": 10,
|
||||
"angle": 90,
|
||||
"width": 0.5,
|
||||
"port_number": 2,
|
||||
"pitch": 12,
|
||||
"pins": [
|
||||
{"name": "input_io1", "role": "io1"},
|
||||
{"name": "input_io2", "role": "io2"},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "input", spec.elements["input"], FakeNazca)
|
||||
|
||||
self.assertEqual(FakePin.created_names, ["input_io1", "input_io1_in", "input_io2", "input_io2_in"])
|
||||
self.assertEqual(FakeCell.put_calls, [("element_input", 5.0, 10.0, 90.0)])
|
||||
self.assertEqual(pin_map[("input", "input_io1")].a, 450.0)
|
||||
self.assertEqual(pin_map[("input", "input_io2")].a, 450.0)
|
||||
self.assertEqual((pin_map[("input", "input_io1")].x, pin_map[("input", "input_io1")].y), (5.0, 16.0))
|
||||
self.assertEqual((pin_map[("input", "input_io2")].x, pin_map[("input", "input_io2")].y), (5.0, 4.0))
|
||||
|
||||
def test_anchor_element_defaults_to_named_a_b_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
class FakePin:
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"anchor_1": {"type": "anchor", "x": 10, "y": 20, "angle": 0, "port_number": 2, "pitch": 12},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "anchor_1", spec.elements["anchor_1"], FakeNazca)
|
||||
|
||||
self.assertIn(("anchor_1", "anchor_1_a1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_a2"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b2"), pin_map)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_a1")].a, 180.0)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_b1")].a, 0.0)
|
||||
|
||||
self.assertEqual((pin_map[("input", "input_io1")].x, pin_map[("input", "input_io1")].y), (5.0, 16.0))
|
||||
self.assertEqual((pin_map[("input", "input_io2")].x, pin_map[("input", "input_io2")].y), (5.0, 4.0))
|
||||
|
||||
def test_anchor_element_defaults_to_named_a_b_pins(self):
|
||||
from mxpic_router.builder import _register_element_pins
|
||||
from mxpic_router.eda_loader import parse_cell_dict
|
||||
|
||||
class FakePlacedPin:
|
||||
def __init__(self, name, x, y, a):
|
||||
self.name = name
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.a = a
|
||||
|
||||
class FakePin:
|
||||
current_cell = None
|
||||
|
||||
def __init__(self, name, width=None, xs=None):
|
||||
self.name = name
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakePlacedPin(self.name, x, y, a)
|
||||
if FakePin.current_cell is not None:
|
||||
FakePin.current_cell.pin[self.name] = placed
|
||||
return placed
|
||||
|
||||
class FakeCell:
|
||||
def __init__(self, name=None, instantiate=True):
|
||||
self.name = name
|
||||
self.pin = {}
|
||||
|
||||
def __enter__(self):
|
||||
FakePin.current_cell = self
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
FakePin.current_cell = None
|
||||
return False
|
||||
|
||||
def put(self, x, y, a):
|
||||
placed = FakeCell(name=f"{self.name}_placed")
|
||||
placed.pin = {
|
||||
name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a)
|
||||
for name, pin in self.pin.items()
|
||||
}
|
||||
return placed
|
||||
|
||||
class FakeNazca:
|
||||
Pin = FakePin
|
||||
Cell = FakeCell
|
||||
|
||||
spec = parse_cell_dict({
|
||||
"name": "cell",
|
||||
"elements": {
|
||||
"anchor_1": {"type": "anchor", "x": 10, "y": 20, "angle": 0, "port_number": 2, "pitch": 12},
|
||||
},
|
||||
})
|
||||
pin_map = {}
|
||||
|
||||
_register_element_pins(pin_map, "anchor_1", spec.elements["anchor_1"], FakeNazca)
|
||||
|
||||
self.assertIn(("anchor_1", "anchor_1_a1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b1"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_a2"), pin_map)
|
||||
self.assertIn(("anchor_1", "anchor_1_b2"), pin_map)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_a1")].a, 180.0)
|
||||
self.assertEqual(pin_map[("anchor_1", "anchor_1_b1")].a, 0.0)
|
||||
|
||||
def test_pdk_metadata_ports_are_registered_as_pins_for_allowed_pdk_roots(self):
|
||||
from mxpic_router.builder import _metadata_pins
|
||||
|
||||
metadata = {"ports": {"a1": {"x": 0, "y": 0, "a": 180}}}
|
||||
|
||||
self.assertEqual(
|
||||
_metadata_pins(metadata, r"D:\mxpic\opt_pdk_public\foundries"),
|
||||
metadata["ports"],
|
||||
|
||||
self.assertEqual(
|
||||
_metadata_pins(metadata, r"D:\mxpic\opt_pdk_public\foundries"),
|
||||
metadata["ports"],
|
||||
)
|
||||
self.assertEqual(_metadata_pins(metadata, r"D:\mxpic\some_project_layout"), {})
|
||||
|
||||
def test_eda_rotation_is_converted_to_layout_rotation_for_metadata_pins(self):
|
||||
from mxpic_router.builder import _layout_rotation, _transform_port
|
||||
|
||||
self.assertEqual(_layout_rotation(90), -90.0)
|
||||
x, y, angle = _transform_port(
|
||||
39.25,
|
||||
0.0,
|
||||
360.0,
|
||||
1719.3,
|
||||
-2080.5,
|
||||
_layout_rotation(90),
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(x, 1719.3)
|
||||
self.assertAlmostEqual(y, -2119.75)
|
||||
self.assertEqual(angle, 270.0)
|
||||
|
||||
def test_perpendicular_pin_rotations_use_straight_bend_straight_route(self):
|
||||
from mxpic_router.builder import _route_method_name_for_pins
|
||||
|
||||
class FakePin:
|
||||
def __init__(self, angle):
|
||||
self.a = angle
|
||||
|
||||
self.assertEqual(
|
||||
_route_method_name_for_pins(FakePin(0), FakePin(90)),
|
||||
"strt_bend_strt_p2p",
|
||||
)
|
||||
self.assertEqual(
|
||||
_route_method_name_for_pins(FakePin(0), FakePin(270)),
|
||||
"strt_bend_strt_p2p",
|
||||
)
|
||||
|
||||
def test_route_backend_falls_back_to_nazca_interconnect_when_forge_is_absent(self):
|
||||
import mxpic_router.builder as builder
|
||||
|
||||
class FakeInterconnect:
|
||||
calls = []
|
||||
|
||||
def __init__(self, radius=None, width=None, xs=None, PCB=False):
|
||||
self.radius = radius
|
||||
self.width = width
|
||||
self.xs = xs
|
||||
self.PCB = PCB
|
||||
|
||||
def strt_p2p(self, **kwargs):
|
||||
self.calls.append(("strt_p2p", kwargs))
|
||||
return "straight"
|
||||
|
||||
def sbend_p2p(self, **kwargs):
|
||||
self.calls.append(("sbend_p2p", kwargs))
|
||||
return "sbend"
|
||||
|
||||
def ubend_p2p(self, **kwargs):
|
||||
self.calls.append(("ubend_p2p", kwargs))
|
||||
return "ubend"
|
||||
|
||||
|
||||
def __init__(self, radius=None, width=None, xs=None, PCB=False):
|
||||
self.radius = radius
|
||||
self.width = width
|
||||
self.xs = xs
|
||||
self.PCB = PCB
|
||||
|
||||
def strt_p2p(self, **kwargs):
|
||||
self.calls.append(("strt_p2p", kwargs))
|
||||
return "straight"
|
||||
|
||||
def sbend_p2p(self, **kwargs):
|
||||
self.calls.append(("sbend_p2p", kwargs))
|
||||
return "sbend"
|
||||
|
||||
def ubend_p2p(self, **kwargs):
|
||||
self.calls.append(("ubend_p2p", kwargs))
|
||||
return "ubend"
|
||||
|
||||
def bend_strt_bend_p2p(self, **kwargs):
|
||||
self.calls.append(("bend_strt_bend_p2p", kwargs))
|
||||
return "bend"
|
||||
|
||||
class FakeInterconnects:
|
||||
Interconnect = FakeInterconnect
|
||||
|
||||
class FakeNazca:
|
||||
interconnects = FakeInterconnects
|
||||
|
||||
original_import = builder._import_mxpic_forge_route
|
||||
original_create = builder._NazcaInterconnectRoute._create_interconnect
|
||||
try:
|
||||
builder._import_mxpic_forge_route = lambda: (_ for _ in ()).throw(ImportError("no forge"))
|
||||
builder._NazcaInterconnectRoute._create_interconnect = staticmethod(
|
||||
lambda **kwargs: FakeInterconnect(**kwargs)
|
||||
)
|
||||
|
||||
route_backend = builder._import_route_backend(FakeNazca)
|
||||
route = route_backend(radius=20, width=0.7, xs="strip", PCB=False)
|
||||
def strt_bend_strt_p2p(self, **kwargs):
|
||||
self.calls.append(("strt_bend_strt_p2p", kwargs))
|
||||
return "straight_bend_straight"
|
||||
|
||||
class FakeInterconnects:
|
||||
Interconnect = FakeInterconnect
|
||||
|
||||
class FakeNazca:
|
||||
interconnects = FakeInterconnects
|
||||
|
||||
original_import = builder._import_mxpic_forge_route
|
||||
original_create = builder._NazcaInterconnectRoute._create_interconnect
|
||||
try:
|
||||
builder._import_mxpic_forge_route = lambda: (_ for _ in ()).throw(ImportError("no forge"))
|
||||
builder._NazcaInterconnectRoute._create_interconnect = staticmethod(
|
||||
lambda **kwargs: FakeInterconnect(**kwargs)
|
||||
)
|
||||
|
||||
route_backend = builder._import_route_backend(FakeNazca)
|
||||
route = route_backend(radius=20, width=0.7, xs="strip", PCB=False)
|
||||
|
||||
self.assertEqual(route.backend_name, "nazca Interconnect")
|
||||
self.assertEqual(route.strt_p2p(pin1="a", pin2="b", arrow=False), "straight")
|
||||
self.assertEqual(
|
||||
route.strt_bend_strt_p2p(pin1="a", pin2="b", radius=12, arrow=False),
|
||||
"straight_bend_straight",
|
||||
)
|
||||
self.assertEqual(route.bend_p2p(pin1="a", pin2="b", radius=10, arrow=False), "bend")
|
||||
self.assertEqual(FakeInterconnect.calls[0][0], "strt_p2p")
|
||||
self.assertEqual(FakeInterconnect.calls[0][1]["width"], 0.7)
|
||||
self.assertEqual(FakeInterconnect.calls[1][0], "bend_strt_bend_p2p")
|
||||
self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 10)
|
||||
self.assertEqual(FakeInterconnect.calls[1][0], "strt_bend_strt_p2p")
|
||||
self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 12)
|
||||
self.assertEqual(FakeInterconnect.calls[2][0], "bend_strt_bend_p2p")
|
||||
self.assertEqual(FakeInterconnect.calls[2][1]["radius"], 10)
|
||||
finally:
|
||||
builder._import_mxpic_forge_route = original_import
|
||||
builder._NazcaInterconnectRoute._create_interconnect = original_create
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user