def test_alloc_triads_single(self): # Should be able to allocate single blocks w, h = 3, 4 next_id = 10 a = Allocator(w, h, next_id=next_id) for _ in range(w * h): allocation_id, boards, periphery, torus = a._alloc_triads(1, 1) assert torus is WrapAround.none assert allocation_id == next_id next_id += 1 assert len(boards) == 3 xys = set((x, y) for (x, y, z) in boards) assert len(xys) == 1 assert set(z for (x, y, z) in boards) == set(range(3)) x, y = xys.pop() assert periphery == set((x, y, z, link) for z in range(3) for link in Links if (board_down_link(x, y, z, link, w, h)[:2] != (x, y))) # Should get full assert a._alloc_triads(1, 1) is None
def test_call_success(self, have_dead_links, have_dead_boards): # If "None" is provided for maximum links/boards we should always # succeed w, h = 10, 9 dead_boards = set([0, 0, 1]) if have_dead_boards else set() dead_links = set([0, 0, 0, Links.north]) if have_dead_links else set() cf = _CandidateFilter(w, h, dead_boards, dead_links, None, None, False) assert cf(0, 0, 1, 1) is True assert cf.boards == set((0, 0, z) for z in range(3)) assert cf.periphery == set( # pragma: no branch (0, 0, z, link) for z in range(3) for link in Links if board_down_link(0, 0, z, link, w, h)[:2] != (0, 0)) assert cf.torus == WrapAround.none
def test_create_job(conn, m): controller = conn._bmp_controllers[m][(0, 0)] with controller.handler_lock: # Make sure newly added jobs can start straight away and have the BMP # block job_id1 = conn.create_job(None, 1, 1, owner="me", keepalive=60.0) # Sane default keepalive should be selected assert conn._jobs[job_id1].keepalive == 60.0 # BMPs should have been told to power-on assert all( set(requests.power_on_boards) == set(range(3)) for requests in controller.add_requests_calls) # Links around the allocation should have been disabled assert all(_e is False for requests in controller.add_requests_calls for _b, _l, _e in requests.link_requests) assert ( # pragma: no branch set((_b, _l) for requests in controller.add_requests_calls for _b, _l, _e in requests.link_requests) == set( (_b, _l) for _b in range(3) for _l in Links if board_down_link(0, 0, _b, _l, 1, 2)[:2] != (0, 0))) # Job should be waiting for power-on since the BMP is blocked time.sleep(0.05) assert conn.get_job_state(None, job_id1).state is JobState.power # Job should be powered on once the BMP process returns time.sleep(0.05) assert conn.get_job_state(None, job_id1).state is JobState.ready # Adding another job which will be queued should result in a job in the # right state. job_id2 = conn.create_job(None, 1, 2, owner="me", keepalive=10.0) assert conn._jobs[job_id2].keepalive == 10.0 assert job_id1 != job_id2 assert conn.get_job_state(None, job_id2).state is JobState.queued # Adding a job which cannot fit should come out immediately cancelled job_id3 = conn.create_job(None, 2, 2, owner="me", keepalive=60.0) assert job_id1 != job_id3 and job_id2 != job_id3 assert conn.get_job_state(None, job_id3).state is JobState.destroyed assert conn.get_job_state(None, job_id3).reason \ == "Cancelled: No suitable machines available."
def _enumerate_boards(self, x, y, width, height): """Starting from board (x, y, 0), enumerate as many reachable and working boards as possible within the rectangle width x height triads. Returns ------- set([(x, y, z), ...]) """ # The set of visited (and working) boards boards = set() to_visit = deque([(x, y, 0)]) while to_visit: x1, y1, z1 = to_visit.popleft() # Skip dead boards and boards we've seen before if ((x1, y1, z1) in self.dead_boards or (x1, y1, z1) in boards): continue boards.add((x1, y1, z1)) # Visit neighbours which are within the range for link in Links: # Skip dead links if (x1, y1, z1, link) in self.dead_links: continue x2, y2, z2, _ = board_down_link(x1, y1, z1, link, self.width, self.height) # Skip links to boards outside the specified range if not (x <= x2 < x + width and y <= y2 < y + height): continue to_visit.append((x2, y2, z2)) # Return the set of boards we could reach return boards
def test_board_down_link(x1, y1, z1, link, width, height, x2, y2, z2, wrapped): assert (board_down_link(x1, y1, z1, link, width, height) == (x2, y2, z2, wrapped))
def _classify_links(self, boards): """Get a list of links of various classes connected to the supplied set of boards. Parameters ---------- boards : set([(x, y, z), ...]) A set of fully-connected, alive boards. Returns ------- alive : set([(x, y, z, :py:class:`rig.links.Links`), ...]) Links which are working and connect one board in the set to another. wrap : set([(x, y, z, :py:class:`rig.links.Links`), ...]) Working links between working boards in the set which wrap-around the toroid. dead : set([(x, y, z, :py:class:`rig.links.Links`), ...]) Links which are not working and connect one board in the set to another. dead_wrap : set([(x, y, z, :py:class:`rig.links.Links`), ...]) Dead links between working boards in the set which wrap-around the toroid. periphery : set([(x, y, z, :py:class:`rig.links.Links`), ...]) Links are those which connect from one board in the set to a board outside the set. These links may be dead or alive. wrap_around_type : :py:class:`~spalloc_server.coordinates.WrapAround` What types of wrap-around links are present (making no distinction between dead and alive links)? """ alive = set() wrap = set() dead = set() dead_wrap = set() periphery = set() wrap_around_type = WrapAround.none for x1, y1, z1 in boards: for link in Links: is_dead = (x1, y1, z1, link) in self.dead_links x2, y2, z2, wrapped = board_down_link(x1, y1, z1, link, self.width, self.height) in_set = (x2, y2, z2) in boards if in_set: wrap_around_type |= wrapped if wrapped: if is_dead: dead_wrap.add((x1, y1, z1, link)) else: wrap.add((x1, y1, z1, link)) else: if is_dead: dead.add((x1, y1, z1, link)) else: alive.add((x1, y1, z1, link)) else: periphery.add((x1, y1, z1, link)) return (alive, wrap, dead, dead_wrap, periphery, WrapAround(wrap_around_type))
def test_enqueue_job(q, on_allocate, on_cancel): # Make sure that the _enqueue_job method does what it should (tested via # calls to create_job). q.add_machine("1x4", 1, 4) q.add_machine("1x2", 1, 2) q.add_machine("1x5", 1, 5) q.add_machine("1x2_pie", 1, 2, tags=set(["pie", "chips"])) q.add_machine("1x2_curry", 1, 2, tags=set(["curry", "chips"])) # If we create a 1x1 job we should get put on the first available machine. # Also verifies the call to on_allocate. q.create_job(1, 1, job_id=100) on_allocate.assert_called_once_with( 100, "1x4", set((0, 0, z) for z in range(3)), set((0, 0, z, link) for z in range(3) for link in Links if board_down_link(0, 0, z, link, 1, 4)[:2] != (0, 0)), True) on_allocate.reset_mock() # We should make use of the first machine which fits and skip over anything # which is full or too small. q.create_job(1, 4, job_id=110) assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 110 assert on_allocate.mock_calls[0][1][1] == "1x5" on_allocate.reset_mock() # We should still be able to fit small things in the first available # machine q.create_job(1, 3, job_id=120) assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 120 assert on_allocate.mock_calls[0][1][1] == "1x4" on_allocate.reset_mock() # Should be able to specify a specific machine q.create_job(1, 1, job_id=130, machine="1x5") assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 130 assert on_allocate.mock_calls[0][1][1] == "1x5" on_allocate.reset_mock() # Fill up the last available (default-tagged) free slot q.create_job(1, 2, job_id=140) assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 140 assert on_allocate.mock_calls[0][1][1] == "1x2" on_allocate.reset_mock() # The next job created should get queued on all default-tagged machines q.create_job(1, 1, job_id=150) assert len(on_allocate.mock_calls) == 0 assert list(q._machines["1x4"].queue) == [q._jobs[150]] assert list(q._machines["1x2"].queue) == [q._jobs[150]] assert list(q._machines["1x5"].queue) == [q._jobs[150]] assert list(q._machines["1x2_pie"].queue) == [] assert list(q._machines["1x2_curry"].queue) == [] # Should be able to create jobs on machines by tag, choosing the first one # when possible q.create_job(1, 1, job_id=160, tags=set(["chips"])) assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 160 assert on_allocate.mock_calls[0][1][1] == "1x2_pie" on_allocate.reset_mock() # Should be able to create jobs on machines by tag, selecting by multiple # tags at once. q.create_job(1, 1, job_id=170, tags=set(["curry", "chips"])) assert len(on_allocate.mock_calls) == 1 assert on_allocate.mock_calls[0][1][0] == 170 assert on_allocate.mock_calls[0][1][1] == "1x2_curry" on_allocate.reset_mock() # Should be able to queue on tagged things q.create_job(1, 2, job_id=180, tags=set(["chips"])) assert list(q._machines["1x4"].queue) == [q._jobs[150]] assert list(q._machines["1x2"].queue) == [q._jobs[150]] assert list(q._machines["1x5"].queue) == [q._jobs[150]] assert list(q._machines["1x2_pie"].queue) == [q._jobs[180]] assert list(q._machines["1x2_curry"].queue) == [q._jobs[180]] # Should be able to queue on all things q.create_job(1, 2, job_id=190, tags=set()) assert list(q._machines["1x4"].queue) == [q._jobs[150], q._jobs[190]] assert list(q._machines["1x2"].queue) == [q._jobs[150], q._jobs[190]] assert list(q._machines["1x5"].queue) == [q._jobs[150], q._jobs[190]] assert list(q._machines["1x2_pie"].queue) == [q._jobs[180], q._jobs[190]] assert list(q._machines["1x2_curry"].queue) == [q._jobs[180], q._jobs[190]] # Should fail if we specify a machine which doesn't exist. q.create_job(1, 1, job_id=200, machine="xxx") on_cancel.assert_called_once_with(200, "No suitable machines available.") on_cancel.reset_mock() # Should fail if we specify a tag doesn't exist. q.create_job(1, 1, job_id=210, tags=set(["xxx"])) on_cancel.assert_called_once_with(210, "No suitable machines available.") on_cancel.reset_mock() # Should fail if we specify impossible requirements for any machine q.create_job(10, 10, job_id=220) on_cancel.assert_called_once_with(220, "No suitable machines available.") on_cancel.reset_mock()