Pass test_workbench_server_condensed

This commit is contained in:
Xavier Bustamante Talavera 2018-06-19 18:38:42 +02:00
parent f00c6f2f49
commit ef132098cb
14 changed files with 240 additions and 130 deletions

View File

@ -29,7 +29,7 @@ class Client(TealClient):
item=None,
headers: dict = None,
token: str = None,
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
res = res.t
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
@ -44,7 +44,7 @@ class Client(TealClient):
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
return super().get(uri, res, query, status, item, accept, headers, token, **kw)
def post(self,
@ -57,7 +57,7 @@ class Client(TealClient):
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
return super().post(data, uri, res, query, status, content_type, accept, headers, token,
**kw)
@ -70,7 +70,7 @@ class Client(TealClient):
res: Union[Type[Union[models.Thing, schemas.Thing]], str],
resources: Iterable[Union[dict, int]],
key: str = None,
**kw) -> Iterable[Union[Dict[str, Any], str]]:
**kw) -> Iterable[Union[Dict[str, object], str]]:
"""Like :meth:`.get` but with many resources."""
return (
self.get(res=res, item=r[key] if key else r, **kw)[0]
@ -106,6 +106,6 @@ class UserClient(Client):
item=None,
headers: dict = None,
token: str = None,
**kw) -> Tuple[Union[Dict[str, Any], str], Response]:
**kw) -> Tuple[Union[Dict[str, object], str], Response]:
return super().open(uri, res, status, query, accept, content_type, item, headers,
self.user['token'] if self.user else token, **kw)

View File

@ -5,10 +5,11 @@ from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DataSto
DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \
MotherboardDef, NetbookDef, NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef, \
SolidStateDriveDef
from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, EventDef, InstallDef, \
from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, BenchmarkDataStorageDef, \
BenchmarkDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef, \
BenchmarkWithRateDef, EraseBasicDef, EraseSectorsDef, EventDef, InstallDef, \
PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \
StepRandomDef, StepZeroDef, TestDataStorageDef, TestDef, WorkbenchRateDef, EraseBasicDef, \
EraseSectorsDef
StepRandomDef, StepZeroDef, StressTestDef, TestDataStorageDef, TestDef, WorkbenchRateDef
from ereuse_devicehub.resources.inventory import InventoryDef
from ereuse_devicehub.resources.tag import TagDef
from ereuse_devicehub.resources.user import OrganizationDef, UserDef
@ -23,7 +24,9 @@ class DevicehubConfig(Config):
OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef,
StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef,
PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef,
TestDataStorageDef, WorkbenchRateDef, InventoryDef
TestDataStorageDef, StressTestDef, WorkbenchRateDef, InventoryDef, BenchmarkDef,
BenchmarkDataStorageDef, BenchmarkWithRateDef, BenchmarkProcessorDef,
BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef
}
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' # type: str

View File

@ -1,13 +1,13 @@
from ereuse_devicehub.marshmallow import NestedOn
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 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 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
@ -52,11 +52,11 @@ class Device(Thing):
@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
from ereuse_devicehub.resources.event.models import EraseBasic, Test, Rate, Install, \
Benchmark
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')
if not isinstance(event, (Install, EraseBasic, Rate, Test, Benchmark)):
raise ValidationError('You cannot upload {}'.format(event), field_names=['events'])
class Computer(Device):

View File

@ -2,14 +2,15 @@ from contextlib import suppress
from itertools import groupby
from typing import Iterable, Set
from sqlalchemy import inspect
from sqlalchemy.util import OrderedSet
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Component, Computer, Device
from ereuse_devicehub.resources.event.models import Remove
from ereuse_devicehub.resources.tag.model import Tag
from sqlalchemy import inspect
from sqlalchemy.exc import IntegrityError
from sqlalchemy.util import OrderedSet
from teal.db import ResourceNotFound
from teal.marshmallow import ValidationError
@ -153,7 +154,10 @@ class Sync:
if device.hid:
with suppress(ResourceNotFound):
db_device = Device.query.filter_by(hid=device.hid).one()
tags = {Tag.query.filter_by(id=tag.id).one() for tag in device.tags} # type: Set[Tag]
try:
tags = {Tag.query.filter_by(id=tag.id).one() for tag in device.tags} # type: Set[Tag]
except ResourceNotFound:
raise ResourceNotFound('tag you are linking to device {}'.format(device))
linked_tags = {tag for tag in tags if tag.device_id} # type: Set[Tag]
if linked_tags:
sample_tag = next(iter(linked_tags))
@ -172,7 +176,18 @@ class Sync:
db.session.add(device)
db_device = device
db_device.tags |= tags # Union of tags the device had plus the (potentially) new ones
db.session.flush()
try:
db.session.flush()
except IntegrityError as e:
# Manage 'one tag per organization' unique constraint
if 'One tag per organization' in e.args[0]:
# todo test for this
id = int(e.args[0][135:e.args[0].index(',', 135)])
raise ValidationError('The device is already linked to tag {} '
'from the same organization.'.format(id),
field_names=['device.tags'])
else:
raise
assert db_device is not None
return db_device

View File

@ -1,9 +1,11 @@
from typing import Callable, Iterable, Tuple
from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, EraseBasic, Event, \
Install, PhotoboxSystemRate, PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, \
StepZero, Test, TestDataStorage, WorkbenchRate, EraseSectors
from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, Benchmark, \
BenchmarkDataStorage, BenchmarkProcessor, BenchmarkProcessorSysbench, BenchmarkRamSysbench, \
BenchmarkWithRate, EraseBasic, EraseSectors, Event, Install, PhotoboxSystemRate, \
PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, StepZero, StressTest, Test, \
TestDataStorage, WorkbenchRate
from ereuse_devicehub.resources.event.views import EventView, SnapshotView
from teal.resource import Converters, Resource
@ -85,3 +87,31 @@ class TestDef(EventDef):
class TestDataStorageDef(TestDef):
SCHEMA = TestDataStorage
class StressTestDef(TestDef):
SCHEMA = StressTest
class BenchmarkDef(EventDef):
SCHEMA = Benchmark
class BenchmarkDataStorageDef(BenchmarkDef):
SCHEMA = BenchmarkDataStorage
class BenchmarkWithRateDef(BenchmarkDef):
SCHEMA = BenchmarkWithRate
class BenchmarkProcessorDef(BenchmarkWithRateDef):
SCHEMA = BenchmarkProcessor
class BenchmarkProcessorSysbenchDef(BenchmarkProcessorDef):
SCHEMA = BenchmarkProcessorSysbench
class BenchmarkRamSysbenchDef(BenchmarkWithRateDef):
SCHEMA = BenchmarkRamSysbench

View File

@ -2,6 +2,14 @@ from collections import Iterable
from typing import Set, Union
from uuid import uuid4
from ereuse_devicehub.db import db
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
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from flask import g
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \
Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event
@ -12,14 +20,6 @@ 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, 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
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \
POLYMORPHIC_ON, StrictVersionType, check_range
@ -46,6 +46,8 @@ class Event(Thing):
closed.comment = """
Whether the author has finished the event.
After this is set to True, no modifications are allowed.
By default are events are closed when performed.
"""
error = Column(Boolean, default=False, nullable=False)
error.comment = """
@ -392,7 +394,7 @@ class StressTest(Test):
class Benchmark(JoinedTableMixin, EventWithOneDevice):
pass
elapsed = Column(Interval)
class BenchmarkDataStorage(Benchmark):

View File

@ -1,9 +1,3 @@
from flask import current_app as app
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, Nested, String, TimeDelta, UUID
from marshmallow.validate import Length, Range
from marshmallow_enum import EnumField
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device.schemas import Component, Device
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
@ -12,6 +6,13 @@ from ereuse_devicehub.resources.event import models as m
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.user.schemas import User
from flask import current_app as app
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \
UUID
from marshmallow.validate import Length, Range
from marshmallow_enum import EnumField
from teal.marshmallow import Version
from teal.resource import Schema
@ -26,6 +27,7 @@ class Event(Thing):
components = NestedOn(Component, dump_only=True, many=True)
description = String(default='', description=m.Event.description.comment)
author = NestedOn(User, dump_only=True, exclude=('token',))
closed = Boolean(missing=True, description=m.Event.closed.comment)
class EventWithOneDevice(Event):
@ -157,7 +159,7 @@ class WorkbenchRate(IndividualRate):
class Install(EventWithOneDevice):
name = String(validate=Length(STR_BIG_SIZE),
name = String(validate=Length(min=4, max=STR_BIG_SIZE),
required=True,
description='The name of the OS installed.')
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
@ -176,12 +178,12 @@ class Snapshot(EventWithOneDevice):
description='The software that generated this Snapshot.')
version = Version(required=True, description='The version of the software.')
events = NestedOn(Event, many=True, dump_only=True)
expected_events = EnumField(SnapshotExpectedEvents,
many=True,
data_key='expectedEvents',
description='Keep open this Snapshot until the following events'
'are performed. Setting this value will activate'
'the async Snapshot.')
expected_events = List(EnumField(SnapshotExpectedEvents),
data_key='expectedEvents',
description='Keep open this Snapshot until the following events'
'are performed. Setting this value will activate'
'the async Snapshot.')
device = NestedOn(Device)
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
components = NestedOn(Component,
@ -217,8 +219,42 @@ class TestDataStorage(Test):
length = EnumField(TestHardDriveLength, required=True)
status = String(validate=Length(max=STR_SIZE), required=True)
lifetime = TimeDelta(precision=TimeDelta.DAYS, required=True)
first_error = Integer()
first_error = Integer(missing=0, data_key='firstError')
passed_lifetime = TimeDelta(precision=TimeDelta.DAYS, data_key='passedLifetime')
assessment = Boolean()
reallocated_sector_count = Integer(data_key='reallocatedSectorCount')
power_cycle_count = Integer(data_key='powerCycleCount')
reported_uncorrectable_errors = Integer(data_key='reportedUncorrectableErrors')
command_timeout = Integer(data_key='commandTimeout')
current_pending_sector_count = Integer(data_key='currentPendingSectorCount')
offline_uncorrectable = Integer(data_key='offlineUncorrectable')
remaining_lifetime_percentage = Integer(data_key='remainingLifetimePercentage')
class StressTest(Test):
pass
class Benchmark(EventWithOneDevice):
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
class BenchmarkDataStorage(Benchmark):
read_speed = Float(required=True, data_key='readSpeed')
write_speed = Float(required=True, data_key='writeSpeed')
class BenchmarkWithRate(Benchmark):
rate = Integer(required=True)
class BenchmarkProcessor(BenchmarkWithRate):
pass
class BenchmarkProcessorSysbench(BenchmarkProcessor):
pass
class BenchmarkRamSysbench(BenchmarkWithRate):
pass

View File

@ -0,0 +1,25 @@
from uuid import UUID
from boltons.urlutils import URL
from sqlalchemy import Column
from sqlalchemy.orm import relationship
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.models import Thing
class Tag(Thing):
id = ... # type: Column
org_id = ... # type: Column
org = ... # type: relationship
provider = ... # type: Column
device_id = ... # type: Column
device = ... # type: relationship
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.id = ... # type: str
self.org_id = ... # type: UUID
self.provider = ... # type: URL
self.device_id = ... # type: int
self.device = ... # type: Device

View File

@ -1,12 +1,11 @@
from flask import Response, current_app as app, request
from marshmallow import Schema
from marshmallow.fields import List, String, URL
from webargs.flaskparser import parser
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.tag import Tag
from teal.marshmallow import ValidationError
from teal.resource import View
from teal.resource import View, Schema
class TagView(View):

View File

@ -9,8 +9,8 @@ type: 'Snapshot'
uuid: 'cb8ce6b5-6a1b-4084-b5b9-d8fadad2a015'
version: '11.0'
software: 'Workbench'
startTime: '2018-06-08T17:52:00'
expectedEvents: ['TestDataStorage', 'StressTest', 'EraseSectors', 'Install']
elapsed: 500
device:
type: 'Microtower'
serialNumber: 'd1s'
@ -19,14 +19,12 @@ device:
tags:
- type: 'Tag'
id: 'tag1'
- type: 'Tag'
id: 'tag2'
events:
- type: 'WorkbenchRate'
appearanceRange: 'A'
functionalityRange: 'B'
- type: 'BenchmarkRamSysbench'
rate: 2444
events:
- type: 'WorkbenchRate'
appearanceRange: 'A'
functionalityRange: 'B'
- type: 'BenchmarkRamSysbench'
rate: 2444
components:
- type: 'GraphicCard'
serialNumber: 'gc1-1s'
@ -43,7 +41,7 @@ components:
- type: 'Processor'
model: 'p1-1s'
manufacturer: 'p1-1mr'
benchmarks:
events:
- type: 'BenchmarkProcessor'
rate: 2410
- type: 'BenchmarkProcessorSysbench'
@ -52,35 +50,36 @@ components:
serialNumber: 'ssd1-1s'
model: 'ssd1-1ml'
manufacturer: 'ssd1-1mr'
benchmark:
type: 'BenchmarkDataStorage'
readingSpeed: 20
writingSpeed: 15
test:
type: 'TestDataStorage'
firstError: 0
error: False
status: 'Completed without error'
length: 'Short'
lifetime: 99
passedLifeTime: 99
assessment: True
powerCycleCount: 11
reallocatedSectorCount: 2
powerCycleCount: 4
reportedUncorrectableErrors: 1
commandTimeout: 11
currentPendingSectorCount: 1
offlineUncorrectable: 33
remainingLifetimePercentage: 1
events:
- type: 'BenchmarkDataStorage'
readSpeed: 20
writeSpeed: 15
elapsed: 21
- type: 'TestDataStorage'
elapsed: 233
firstError: 0
error: False
status: 'Completed without error'
length: 'Short'
lifetime: 99
passedLifetime: 99
assessment: True
powerCycleCount: 11
reallocatedSectorCount: 2
powerCycleCount: 4
reportedUncorrectableErrors: 1
commandTimeout: 11
currentPendingSectorCount: 1
offlineUncorrectable: 33
remainingLifetimePercentage: 1
- type: 'HardDrive'
serialNumber: 'hdd1-1s'
model: 'hdd1-1ml'
manufacturer: 'hdd1-1mr'
benchmark:
type: 'BenchmarkDataStorage'
readingSpeed: 10
writingSpeed: 5
events:
- type: 'BenchmarkDataStorage'
readSpeed: 10
writeSpeed: 5
- type: 'Motherboard'
serialNumber: 'mb1-1s'
model: 'mb1-1ml'

View File

@ -9,4 +9,4 @@
type: 'StressTest'
elapsed: 300
error: False
snapshot: None # fulfill!
# snapshot: None fulfill!

View File

@ -8,8 +8,8 @@
type: 'EraseSectors'
error: False
snapshot: None # fulfill!
device: None # fulfill!
# snapshot: None fulfill!
# device: None fulfill!
cleanWithZeros: False
startTime: 2018-01-01T10:10:10
endTime: 2018-01-01T12:10:10
@ -19,3 +19,5 @@ steps:
startTime: '2018-01-01T10:10:10'
endTime: '2018-01-01T12:10:10'
error: False
cleanWithZeros: False
secureRandomSteps: 0

View File

@ -9,6 +9,6 @@
type: 'Install'
elapsed: 420
error: False
snapshot: None # fulfill!
device: None # fulfill!
# snapshot: None fulfill!
# device: None fulfill!
name: 'LinuxMint 18.01 32b'

View File

@ -1,13 +1,52 @@
"""
Tests that emulates the behaviour of a WorkbenchServer.
"""
import pytest
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.event.models import EraseSectors, Install, Snapshot, \
StressTest
from ereuse_devicehub.resources.tag.model import Tag
from tests.conftest import file
def test_workbench_server_condensed(user: UserClient):
"""
As :def:`.test_workbench_server_phases` but all the events
condensed in only one big ``Snapshot`` file, as described
in the docs.
"""
s = file('workbench-server-1.snapshot')
del s['expectedEvents']
s['device']['events'].append(file('workbench-server-2.stress-test'))
s['components'][4]['events'].extend((
file('workbench-server-3.erase'),
file('workbench-server-4.install')
))
s['components'][5]['events'] = [file('workbench-server-3.erase')]
# Create tags
user.post(res=Tag, query=[('ids', t['id']) for t in s['device']['tags']], data={})
snapshot, _ = user.post(res=Snapshot, data=s)
events = snapshot['events']
assert {(event['type'], event['device']) for event in events} == {
# todo missing Rate event aggregating the rates
('WorkbenchRate', 1),
('BenchmarkProcessorSysbench', 5),
('StressTest', 1),
('EraseSectors', 6),
('BenchmarkRamSysbench', 1),
('BenchmarkProcessor', 5),
('Install', 6),
('EraseSectors', 7),
('BenchmarkDataStorage', 6),
('TestDataStorage', 6)
}
assert snapshot['closed']
assert not snapshot['error']
@pytest.mark.xfail(reason='Functionality not yet developed.')
def test_workbench_server_phases(user: UserClient):
"""
Tests the phases described in the docs section `Snapshots from
@ -73,43 +112,3 @@ def test_workbench_server_phases(user: UserClient):
pc, _ = user.get(res=Device, item=snapshot['id'])
assert len(pc['events']) == 10 # todo shall I add child events?
def test_workbench_server_condensed(user: UserClient):
"""
As :def:`.test_workbench_server_phases` but all the events
condensed in only one big ``Snapshot`` file, as described
in the docs.
"""
s = file('workbench-server-1.snapshot')
s['events'].append(file('workbench-server-2.stress-test'))
s['components'][5]['erasure'] = file('workbench-server-3.erase')
s['components'][5]['installation'] = file('workbench-server-4.install')
s['components'][6]['erasure'] = file('workbench-server-3.erase')
snapshot, _ = user.post(res=Snapshot, data=s)
events = snapshot['events']
assert events[0]['type'] == 'Rate'
assert events[0]['device'] == 1
assert events[0]['closed']
assert events[0]['type'] == 'WorkbenchRate'
assert events[0]['device'] == 1
assert events[1]['type'] == 'BenchmarkProcessor'
assert events[1]['device'] == 5
assert events[2]['type'] == 'BenchmarkProcessorSysbench'
assert events[2]['device'] == 5
assert events[3]['type'] == 'BenchmarkDataStorage'
assert events[3]['device'] == 6
assert events[4]['type'] == 'TestDataStorage'
assert events[4]['device'] == 6
assert events[4]['type'] == 'BenchmarkDataStorage'
assert events[4]['device'] == 7
assert events[5]['type'] == 'StressTest'
assert events[5]['device'] == 1
assert events[6]['type'] == 'EraseSectors'
assert events[6]['device'] == 6
assert events[7]['type'] == 'EraseSectors'
assert events[7]['device'] == 7
assert events[8]['type'] == 'Install'
assert events[8]['device'] == 6
assert snapshot['closed']
assert not snapshot['error']