From d2af894174cea4da7cb69da1920262fb6dceb625 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sat, 16 Jun 2018 12:41:12 +0200 Subject: [PATCH] Sync snapshot; add event listeners to auto-update device relationships --- docs/events.rst | 37 ++++--- docs/inventory.rst | 6 +- docs/tags.rst | 14 +-- ereuse_devicehub/resources/device/models.py | 4 + ereuse_devicehub/resources/device/models.pyi | 15 +-- ereuse_devicehub/resources/device/schemas.py | 29 ++++- ereuse_devicehub/resources/device/sync.py | 2 +- ereuse_devicehub/resources/event/models.py | 106 ++++++++++++++++--- ereuse_devicehub/resources/event/models.pyi | 8 ++ ereuse_devicehub/resources/event/schemas.py | 7 +- ereuse_devicehub/resources/event/views.py | 47 ++++++-- tests/files/basic.snapshot.yaml | 12 +-- tests/files/erase-sectors.snapshot.yaml | 40 +++---- tests/test_event.py | 77 +++++++++++++- tests/test_snapshot.py | 39 ++++--- 15 files changed, 332 insertions(+), 111 deletions(-) diff --git a/docs/events.rst b/docs/events.rst index 16a2fe04..23aac2c9 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -1,5 +1,5 @@ Events -====== +###### .. toctree:: :maxdepth: 4 @@ -8,7 +8,7 @@ Events Rate ----- +**** Devicehub generates an rating for a device taking into consideration the visual, functional, and performance. @@ -73,7 +73,7 @@ The same ``ImageSet`` can be rated multiple times, generating a new .. todo:: which info does photobox provide for each picture? Snapshot --------- +******** The Snapshot sets the physical information of the device (S/N, model...) and updates it with erasures, benchmarks, ratings, and tests; updates the composition of its components (adding / removing them), and links tags @@ -106,10 +106,16 @@ a device: perform ``Remove`` on the old parent. Snapshots from Workbench -~~~~~~~~~~~~~~~~~~~~~~~~ +======================== When processing a device from the Workbench, this one performs a Snapshot and then performs more events (like testings, benchmarking...). +There are two ways of sending this information. In an async way, +this is, submitting events as soon as Workbench performs then, or +submitting only one Snapshot event with all the other events embedded. + +Asynced +------- The use case, which is represented in the ``test_workbench_phases``, is as follows: @@ -121,10 +127,11 @@ is as follows: - Identification information about the device and components (S/N, model, physical characteristics...) - - Tags. - - Rate. - - Benchmarks. - - TestDataStorage. + - ``Tags`` in a ``tags`` property in the ``device``. + - ``Rate`` in an ``events`` property in the ``device``. + - ``Benchmarks`` in an ``events`` property in each ``component`` + or ``device``. + - ``TestDataStorage`` as in ``Benchmarks``. - An ordered set of **expected events**, defining which are the next events that Workbench will perform to the device in ideal conditions (device doesn't fail, no Internet drop...). @@ -147,18 +154,14 @@ is as follows: 5. In **T3+Tn+Tx**, when all *expected events* have been performed, Devicehub **closes** the ``Snapshot`` from 1. +Synced +------ Optionally, Devicehub understands receiving a ``Snapshot`` with all -the events the following way: - -- ``Install`` embedded in a ``installation`` field in its respective - ``DataStorage`` component in the ``Snapshot``. -- ``Erase`` embedded in ``erasure`` field in its respective - ``DataStorage`` in the ``Snapshot``. -- ``StressTest`` in an ``events`` field in the ``Snapshot``. - +the events in an ``events`` property inside each affected ``component`` +or ``device``. ToDispose and DisposeProduct ----------------------------- +**************************** There are four events for getting rid of devices: - ``ToDispose``: The device is marked to be disposed. diff --git a/docs/inventory.rst b/docs/inventory.rst index cf86c081..9697bcc1 100644 --- a/docs/inventory.rst +++ b/docs/inventory.rst @@ -1,5 +1,5 @@ Inventory -======= +######### Devicehub uses the same path to get devices and lots. @@ -17,7 +17,7 @@ groups in a page. Select the actual page by ``GET /inventory?page=3``. By default you get the page number ``1``. Query ------ +***** The query consists of 4 optional params: - **search**: Filters devices by performing a full-text search over their @@ -47,7 +47,7 @@ The query consists of 4 optional params: By default is ``1``; the first page. Result ------- +****** The result is a JSON object with the following fields: - **devices**: A list of devices. diff --git a/docs/tags.rst b/docs/tags.rst index 5d37cfdd..7bc15056 100644 --- a/docs/tags.rst +++ b/docs/tags.rst @@ -1,5 +1,5 @@ Tags -==== +#### Devicehub can generate tags, which are synthetic identifiers that identify a device in an organization. A tag has minimally two fields: the ID and the Registration Number of the organization that generated @@ -26,7 +26,7 @@ Note that these virtual tags don't have to forcefully be printed or have a physical representation (this is not imposed at system level). The eReuse.org tags (eTag) --------------------------- +************************** We recognize a special type of tag, the **eReuse.org tags (eTag)**. These are tags defined by eReuse.org and that can be issued only by tag providers that comply with the eReuse.org requisites. @@ -41,7 +41,7 @@ software, eReuse.org certified tag providers can create and manage the tags, and send them to Devicehubs of their choice. Tag ID design -~~~~~~~~~~~~~ +============= The eTag has a fixed schema for its ID: ``XXX-YYYYYYYYYYYYYY``, where: - *XX* is the **eReuse.org Tag Provider ID (eTagPId)**. @@ -59,7 +59,7 @@ As an example, ``FO-A4CZ2`` is a tag from the ``FO`` tag provider and ID ``A4CZ2``. Creating tags -------------- +************* You need to create a tag before linking it to a device. There are two ways of creating a tag: @@ -74,7 +74,7 @@ two ways of creating a tag: Note that tags cannot have a slash ``/``. Linking a tag -------------- +************* Linking a tag is joining the tag with the device. In Devicehub this process is done when performing a Snapshot (POST @@ -91,14 +91,14 @@ too in finding devices when these don't generate a ``HID``. Find more in the ``Snapshot`` docs. Getting a device through its tag --------------------------------- +******************************** When performing ``GET /tags//device`` you will get directly the device of such tag, as long as there are not two tags with the same tag-id. In such case you should use ``GET /tags///device`` to inequivocally get the correct device (to develop). Tags and migrations -------------------- +******************* Tags travel with the devices they are linked when migrating them. Future implementations can parameterize this. diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 6f025ffc..7cd1e067 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -83,6 +83,10 @@ class Device(Thing): class Computer(Device): id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + @property + def events(self) -> list: + return sorted(chain(super().events, self.events_parent), key=attrgetter('created')) + class Desktop(Computer): pass diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index 39ed6226..bac400f5 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -3,7 +3,7 @@ from typing import Dict, List, Set from colour import Color from sqlalchemy import Column -from ereuse_devicehub.resources.enums import RamInterface, RamFormat, DataStorageInterface +from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ EventWithOneDevice from ereuse_devicehub.resources.image.models import ImageList @@ -49,6 +49,7 @@ class Computer(Device): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.components = ... # type: Set[Component] + self.events_parent = ... # type: Set[Event] class Desktop(Computer): @@ -92,12 +93,12 @@ class GraphicCard(Component): class DataStorage(Component): size = ... # type: Column - interface = ... # type: Column + interface = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.size = ... # type: int - self.interface = ... # type: DataStorageInterface + self.interface = ... # type: DataStorageInterface class HardDrive(DataStorage): @@ -147,12 +148,12 @@ class Processor(Component): class RamModule(Component): size = ... # type: Column speed = ... # type: Column - interface = ... # type: Column - format = ... # type: Column + interface = ... # type: Column + format = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.size = ... # type: int self.speed = ... # type: float - self.interface = ... # type: RamInterface - self.format = ... # type: RamFormat + self.interface = ... # type: RamInterface + self.format = ... # type: RamFormat diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 9968b6f0..87faacd6 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,12 +1,14 @@ +from marshmallow import post_load, pre_load from marshmallow.fields import Float, Integer, Str from marshmallow.validate import Length, OneOf, Range from marshmallow_enum import EnumField from sqlalchemy.util import OrderedSet from ereuse_devicehub.marshmallow import NestedOn -from ereuse_devicehub.resources.enums import RamInterface, RamFormat +from ereuse_devicehub.resources.enums import RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes +from teal.marshmallow import ValidationError class Device(Thing): @@ -30,6 +32,31 @@ class Device(Thing): unit=UnitCodes.m, description='The height of the device in meters.') events = NestedOn('Event', many=True, dump_only=True) + events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) + + @pre_load + def from_events_to_events_one(self, data: dict): + """ + Not an elegant way of allowing submitting events to a device + (in the context of Snapshots) without creating an ``events`` + field at the model (which is not possible). + :param data: + :return: + """ + # Note that it is secure to allow uploading events_one + # as the only time an user can send a device object is + # in snapshots. + data['events_one'] = data.pop('events', []) + return data + + @post_load + def validate_snapshot_events(self, data): + """Validates that only snapshot-related events can be uploaded.""" + from ereuse_devicehub.resources.event.models import EraseBasic, Test, Rate, Install + for event in data['events_one']: + if not isinstance(event, (Install, EraseBasic, Rate, Test)): + raise ValidationError('You cannot upload {}'.format(event['type']), + field_names='events') class Computer(Device): diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index daaec583..7eb9d5a9 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -108,7 +108,7 @@ class Sync: blacklist.add(db_component.id) except ResourceNotFound: db.session.add(component) - db.session.flush() + # db.session.flush() db_component = component is_new = True else: diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index fcf15f28..b6d63a4b 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -1,3 +1,5 @@ +from collections import Iterable +from typing import Set, Union from uuid import uuid4 from flask import g @@ -7,10 +9,11 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.orm import backref, relationship +from sqlalchemy.orm.events import AttributeEvents as Events from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Component, DataStorage, Device +from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \ FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \ SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength @@ -96,6 +99,20 @@ class Event(Thing): relationship is filled with the components the computer had at the time of the event. """ + parent_id = Column(BigInteger, ForeignKey(Computer.id)) + parent = relationship(Computer, + backref=backref('events_parent', + lazy=True, + order_by=lambda: Event.created, + collection_class=OrderedSet), + primaryjoin=parent_id == Computer.id) + """ + For events that are performed to components, the device parent + at that time. + + For example: for a ``EraseBasic`` performed on a data storage, this + would point to the computer that contained this data storage, if any. + """ # noinspection PyMethodParameters @declared_attr @@ -131,7 +148,7 @@ class EventWithOneDevice(Event): primaryjoin=Device.id == device_id) def __repr__(self) -> str: - return '<{0.t} {0.id!r} device={0.device_id}>'.format(self) + return '<{0.t} {0.id!r} device={0.device!r}>'.format(self) class EventWithMultipleDevices(Event): @@ -141,7 +158,8 @@ class EventWithMultipleDevices(Event): order_by=lambda: EventWithMultipleDevices.created, collection_class=OrderedSet), secondary=lambda: EventDevice.__table__, - order_by=lambda: Device.id) + order_by=lambda: Device.id, + collection_class=OrderedSet) def __repr__(self) -> str: return '<{0.t} {0.id!r} devices={0.devices!r}>'.format(self) @@ -182,6 +200,10 @@ class EraseBasic(JoinedTableMixin, EventWithOneDevice): clean_with_zeros = Column(Boolean, nullable=False) +class Ready(EventWithMultipleDevices): + pass + + class EraseSectors(EraseBasic): pass @@ -394,16 +416,70 @@ class BenchmarkRamSysbench(BenchmarkWithRate): # Listeners -@event.listens_for(TestDataStorage.device, 'set', retval=True, propagate=True) -@event.listens_for(Install.device, 'set', retval=True, propagate=True) -@event.listens_for(EraseBasic.device, 'set', retval=True, propagate=True) -def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator): - if not isinstance(value, DataStorage): - raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value)) - return value +# Listeners validate values and keep relationships synced -# todo finish adding events -# @event.listens_for(Install.snapshot, 'before_insert', propagate=True) -# def validate_required_snapshot(mapper, connection, target: Event): -# if not target.snapshot: -# raise ValidationError('{0!r} must be linked to a Snapshot.'.format(target)) +@event.listens_for(TestDataStorage.device, Events.set.__name__, propagate=True) +@event.listens_for(Install.device, Events.set.__name__, propagate=True) +@event.listens_for(EraseBasic.device, Events.set.__name__, propagate=True) +def validate_device_is_data_storage(target: Event, value: DataStorage, old_value, initiator): + """Validates that the device for data-storage events is effectively a data storage.""" + if value and not isinstance(value, DataStorage): + raise TypeError('{} must be a DataStorage but you passed {}'.format(initiator.impl, value)) + + +# The following listeners keep relationships with device <-> components synced with the event +# So, if you add or remove devices from events these listeners will +# automatically add/remove the ``components`` and ``parent`` of such events +# See the tests for examples + +@event.listens_for(EventWithOneDevice.device, Events.set.__name__, propagate=True) +def update_components_event_one(target: EventWithOneDevice, device: Device, __, ___): + """ + Syncs the :attr:`.Event.components` with the components in + :attr:`ereuse_devicehub.resources.device.models.Computer.components`. + """ + target.components.clear() + if isinstance(device, Computer): + target.components |= device.components + + +@event.listens_for(EventWithMultipleDevices.devices, Events.init_collection.__name__, + propagate=True) +@event.listens_for(EventWithMultipleDevices.devices, Events.bulk_replace.__name__, propagate=True) +@event.listens_for(EventWithMultipleDevices.devices, Events.append.__name__, propagate=True) +def update_components_event_multiple(target: EventWithMultipleDevices, + value: Union[Set[Device], Device], _): + """ + Syncs the :attr:`.Event.components` with the components in + :attr:`ereuse_devicehub.resources.device.models.Computer.components`. + """ + target.components.clear() + devices = value if isinstance(value, Iterable) else {value} + for device in devices: + if isinstance(device, Computer): + target.components |= device.components + + +@event.listens_for(EventWithMultipleDevices.devices, Events.remove.__name__, propagate=True) +def remove_components_event_multiple(target: EventWithMultipleDevices, device: Device, __): + """ + Syncs the :attr:`.Event.components` with the components in + :attr:`ereuse_devicehub.resources.device.models.Computer.components`. + """ + target.components.clear() + for device in target.devices - {device}: + if isinstance(device, Computer): + target.components |= device.components + + +@event.listens_for(EraseBasic.device, Events.set.__name__, propagate=True) +@event.listens_for(Test.device, Events.set.__name__, propagate=True) +@event.listens_for(Install.device, Events.set.__name__, propagate=True) +@event.listens_for(Benchmark.device, Events.set.__name__, propagate=True) +def update_parent(target: Union[EraseBasic, Test, Install], device: Device, _, __): + """ + Syncs the :attr:`Event.parent` with the parent of the device. + """ + target.parent = None + if isinstance(device, Component): + target.parent = device.parent diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index f4432bc8..48923df9 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -29,6 +29,8 @@ class Event(Thing): author_id = ... # type: Column author = ... # type: relationship components = ... # type: relationship + parent_id = ... # type: Column + parent = ... # type: relationship def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @@ -45,6 +47,8 @@ class Event(Thing): self.author_id = ... # type: UUID self.author = ... # type: User self.components = ... # type: Set[Component] + self.parent_id = ... # type: Computer + self.parent = ... # type: Computer class EventWithOneDevice(Event): @@ -214,6 +218,10 @@ class EraseBasic(EventWithOneDevice): self.success = ... # type: bool +class Ready(EventWithMultipleDevices): + pass + + class EraseSectors(EraseBasic): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 42ef6d3e..1510d249 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -29,11 +29,11 @@ class Event(Thing): class EventWithOneDevice(Event): - device = NestedOn(Device, only='id') + device = NestedOn(Device) class EventWithMultipleDevices(Event): - devices = NestedOn(Device, many=True, only='id') + devices = NestedOn(Device, many=True) class Add(EventWithOneDevice): @@ -73,7 +73,6 @@ class EraseSectors(EraseBasic): class Step(Schema): - id = Integer(dump_only=True) type = String(description='Only required when it is nested.') start_time = DateTime(required=True, data_key='startTime') end_time = DateTime(required=True, data_key='endTime') @@ -176,7 +175,7 @@ class Snapshot(EventWithOneDevice): required=True, description='The software that generated this Snapshot.') version = Version(required=True, description='The version of the software.') - events = NestedOn(Event, many=True) # todo ensure only specific events are submitted + events = NestedOn(Event, many=True, dump_only=True) expected_events = EnumField(SnapshotExpectedEvents, many=True, data_key='expectedEvents', diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index cd68d5a8..14067471 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -1,13 +1,14 @@ from distutils.version import StrictVersion +from typing import List from uuid import UUID from flask import request from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Computer -from ereuse_devicehub.resources.enums import SnapshotSoftware -from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage +from ereuse_devicehub.resources.device.models import Component, Computer +from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware +from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage, WorkbenchRate from teal.resource import View @@ -33,18 +34,42 @@ class SnapshotView(View): # model object, when we flush them to the db we will flush # snapshot, and we want to wait to flush snapshot at the end device = s.pop('device') # type: Computer - components = s.pop('components') if s['software'] == SnapshotSoftware.Workbench else None - if 'events' in s: - events = s.pop('events') - # todo perform events - # noinspection PyArgumentList + components = s.pop('components') \ + if s['software'] == SnapshotSoftware.Workbench else None # type: List[Component] snapshot = Snapshot(**s) - snapshot.device, snapshot.events = self.resource_def.sync.run(device, components) - snapshot.components = snapshot.device.components - # todo compute rating + + # Remove new events from devices so they don't interfere with sync + events_device = set(e for e in device.events_one) + events_components = tuple(set(e for e in component.events_one) for component in components) + device.events_one.clear() + for component in components: + component.events_one.clear() + + # noinspection PyArgumentList + assert not device.events_one + assert all(not c.events_one for c in components) + db_device, remove_events = self.resource_def.sync.run(device, components) + snapshot.device = db_device + snapshot.events |= remove_events | events_device # commit will change the order of the components by what # the DB wants. Let's get a copy of the list so we preserve order ordered_components = OrderedSet(x for x in snapshot.components) + + for event in events_device: + if isinstance(event, WorkbenchRate): + # todo process workbench rate + event.data_storage = 2 + event.graphic_card = 4 + event.processor = 1 + event.algorithm_software = RatingSoftware.Ereuse + event.algorithm_version = StrictVersion('1.0') + + # Add the new events to the db-existing devices and components + db_device.events_one |= events_device + for component, events in zip(ordered_components, events_components): + component.events_one |= events + snapshot.events |= events + db.session.add(snapshot) db.session.commit() # todo we are setting snapshot dirty again with this components but diff --git a/tests/files/basic.snapshot.yaml b/tests/files/basic.snapshot.yaml index afa9ea6b..9459dc1d 100644 --- a/tests/files/basic.snapshot.yaml +++ b/tests/files/basic.snapshot.yaml @@ -2,18 +2,18 @@ type: 'Snapshot' uuid: 'f5efd26e-8754-46bc-87bf-fbccc39d60d9' version: '11.0' software: 'Workbench' -events: - - type: 'WorkbenchRate' - appearanceRange: 'A' - functionalityRange: 'B' - labelling: True - bios: 'B' elapsed: 4 device: type: 'Microtower' serialNumber: 'd1s' model: 'd1ml' manufacturer: 'd1mr' + events: + - type: 'WorkbenchRate' + appearanceRange: 'A' + functionalityRange: 'B' + labelling: True + bios: 'B' components: - type: 'GraphicCard' serialNumber: 'gc1s' diff --git a/tests/files/erase-sectors.snapshot.yaml b/tests/files/erase-sectors.snapshot.yaml index f84e50ba..838caddc 100644 --- a/tests/files/erase-sectors.snapshot.yaml +++ b/tests/files/erase-sectors.snapshot.yaml @@ -12,26 +12,26 @@ components: - type: 'SolidStateDrive' serialNumber: 'c1s' model: 'c1ml' - manufacturer: 'pc1mr' - erasure: - type: 'EraseSectors' - cleanWithZeros: True - startTime: '2018-06-01T08:12:06' - endTime: '2018-06-01T09:12:06' - secureRandomSteps: 20 - steps: - - type: 'StepZero' - error: False - startTime: '2018-06-01T08:15:00' - endTime: '2018-06-01T09:16:00' - secureRandomSteps: 1 - cleanWithZeros: True - - type: 'StepZero' - error: False - startTime: '2018-06-01T08:16:00' - endTime: '2018-06-01T09:17:00' - secureRandomSteps: 1 - cleanWithZeros: True + manufacturer: 'c1mr' + events: + - type: 'EraseSectors' + cleanWithZeros: True + startTime: '2018-06-01T08:12:06' + endTime: '2018-06-01T09:12:06' + secureRandomSteps: 20 + steps: + - type: 'StepZero' + error: False + startTime: '2018-06-01T08:15:00' + endTime: '2018-06-01T09:16:00' + secureRandomSteps: 1 + cleanWithZeros: True + - type: 'StepZero' + error: False + startTime: '2018-06-01T08:16:00' + endTime: '2018-06-01T09:17:00' + secureRandomSteps: 1 + cleanWithZeros: True - type: 'GraphicCard' serialNumber: 'gc1s' model: 'gc1ml' diff --git a/tests/test_event.py b/tests/test_event.py index 0996466d..0f22f19e 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -2,13 +2,14 @@ from datetime import datetime, timedelta import pytest from flask import g +from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, \ - SolidStateDrive +from ereuse_devicehub.resources.device.models import Device, GraphicCard, HardDrive, Microtower, \ + RamModule, SolidStateDrive from ereuse_devicehub.resources.enums import TestHardDriveLength -from ereuse_devicehub.resources.event.models import EraseBasic, EraseSectors, \ - EventWithOneDevice, Install, StepZero, TestDataStorage +from ereuse_devicehub.resources.event.models import BenchmarkDataStorage, EraseBasic, EraseSectors, \ + EventWithOneDevice, Install, Ready, StepZero, StressTest, TestDataStorage from tests.conftest import create_user @@ -125,3 +126,71 @@ def test_install(): device=hdd) db.session.add(install) db.session.commit() + + +@pytest.mark.usefixtures('auth_app_context') +def test_update_components_event_one(): + computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1') + hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') + computer.components.add(hdd) + + # Add event + test = StressTest(elapsed=timedelta(seconds=1)) + computer.events_one.add(test) + assert test.device == computer + assert next(iter(test.components)) == hdd, 'Event has to have new components' + + # Remove event + computer.events_one.clear() + assert not test.device + assert not test.components, 'Event has to loose the components' + + # If we add a component to a device AFTER assigning the event + # to the device, the event doesn't get the new component + computer.events_one.add(test) + ram = RamModule() + computer.components.add(ram) + assert len(test.components) == 1 + + +@pytest.mark.usefixtures('auth_app_context') +def test_update_components_event_multiple(): + computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1') + hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') + computer.components.add(hdd) + + ready = Ready() + assert not ready.devices + assert not ready.components + + # Add + computer.events_multiple.add(ready) + assert ready.devices == OrderedSet([computer]) + assert next(iter(ready.components)) == hdd + + # Remove + computer.events_multiple.remove(ready) + assert not ready.devices + assert not ready.components + + # init / replace collection + ready.devices = OrderedSet([computer]) + assert ready.devices + assert ready.components + + +@pytest.mark.usefixtures('auth_app_context') +def test_update_parent(): + computer = Microtower(serial_number='sn1', model='ml1', manufacturer='mr1') + hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') + computer.components.add(hdd) + + # Add + benchmark = BenchmarkDataStorage() + benchmark.device = hdd + assert benchmark.parent == computer + assert not benchmark.components + + # Remove + benchmark.device = None + assert not benchmark.parent diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 994fc282..f41b73ad 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -74,7 +74,7 @@ def snapshot_and_check(user: UserClient, 'Components must be in their parent' if perform_second_snapshot: input_snapshot['uuid'] = uuid4() - return snapshot_and_check(user, input_snapshot, perform_second_snapshot=False) + return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False) else: return snapshot @@ -126,18 +126,27 @@ def test_snapshot_schema(app: Devicehub): def test_snapshot_post(user: UserClient): """ - Tests the post snapshot endpoint (validation, etc) - and data correctness. + Tests the post snapshot endpoint (validation, etc), data correctness, + and relationship correctness. """ - snapshot = snapshot_and_check(user, file('basic.snapshot'), perform_second_snapshot=False) + snapshot = snapshot_and_check(user, file('basic.snapshot'), + event_types=('WorkbenchRate',), + perform_second_snapshot=False) assert snapshot['software'] == 'Workbench' assert snapshot['version'] == '11.0' assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9' - assert snapshot['events'] == [] assert snapshot['elapsed'] == 4 assert snapshot['author']['id'] == user.user['id'] assert 'events' not in snapshot['device'] assert 'author' not in snapshot['device'] + device, _ = user.get(res=Device, item=snapshot['device']['id']) + assert snapshot['components'] == device['components'] + + assert tuple(c['type'] for c in snapshot['components']) == ('GraphicCard', 'RamModule') + rate, _ = user.get(res=Event, item=snapshot['events'][0]['id']) + assert rate['device']['id'] == snapshot['device']['id'] + assert rate['components'] == snapshot['components'] + assert rate['snapshot']['id'] == snapshot['id'] def test_snapshot_component_add_remove(user: UserClient): @@ -279,7 +288,7 @@ def test_snapshot_tag_inner_tag(tag_id: str, user: UserClient, app: Devicehub): """Tests a posting Snapshot with a local tag.""" b = file('basic.snapshot') b['device']['tags'] = [{'type': 'Tag', 'id': tag_id}] - snapshot_and_check(user, b) + snapshot_and_check(user, b, event_types=('WorkbenchRate',)) with app.app_context(): tag, *_ = Tag.query.all() # type: Tag assert tag.device_id == 1, 'Tag should be linked to the first device' @@ -303,16 +312,17 @@ def test_erase(user: UserClient): storage, *_ = snapshot['components'] assert storage['type'] == 'SolidStateDrive', 'Components must be ordered by input order' storage, _ = user.get(res=SolidStateDrive, item=storage['id']) # Let's get storage events too - _snapshot1, _snapshot2, erasure = storage['events'] - assert erasure['type'] == 'EraseSectors' + # order: creation time descending + _snapshot1, erasure1, _snapshot2, erasure2 = storage['events'] + assert erasure1['type'] == erasure2['type'] == 'EraseSectors' assert _snapshot1['type'] == _snapshot2['type'] == 'Snapshot' - assert snapshot == _snapshot2 - erasure, _ = user.get(res=EraseBasic, item=erasure['id']) + assert snapshot == user.get(res=Event, item=_snapshot2['id'])[0] + erasure, _ = user.get(res=EraseBasic, item=erasure1['id']) assert len(erasure['steps']) == 2 - assert erasure['steps'][0]['startingTime'] == '2018-06-01T08:15:00' - assert erasure['steps'][0]['endingTime'] == '2018-06-01T09:16:00' - assert erasure['steps'][1]['endingTime'] == '2018-06-01T08:16:00' - assert erasure['steps'][1]['endingTime'] == '2018-06-01T09:17:00' + assert erasure['steps'][0]['startTime'] == '2018-06-01T08:15:00+00:00' + assert erasure['steps'][0]['endTime'] == '2018-06-01T09:16:00+00:00' + assert erasure['steps'][1]['startTime'] == '2018-06-01T08:16:00+00:00' + assert erasure['steps'][1]['endTime'] == '2018-06-01T09:17:00+00:00' assert erasure['device']['id'] == storage['id'] for step in erasure['steps']: assert step['type'] == 'StepZero' @@ -320,4 +330,3 @@ def test_erase(user: UserClient): assert step['secureRandomSteps'] == 1 assert step['cleanWithZeros'] is True assert 'num' not in step - assert step['erasure'] == erasure['id']