Sync snapshot; add event listeners to auto-update device relationships

This commit is contained in:
Xavier Bustamante Talavera 2018-06-16 12:41:12 +02:00
parent 5538e5ac69
commit d2af894174
15 changed files with 332 additions and 111 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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/<tag-id>/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/<ngo>/<tag-id>/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.

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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',

View file

@ -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

View file

@ -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'

View file

@ -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'

View file

@ -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

View file

@ -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']