diff --git a/org.oniroproject.integration-tests/COPYING b/org.oniroproject.integration-tests/COPYING
new file mode 100644
index 0000000000000000000000000000000000000000..94a9ed024d3859793618152ea559a168bbcbb5e2
--- /dev/null
+++ b/org.oniroproject.integration-tests/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/org.oniroproject.integration-tests/README.rst b/org.oniroproject.integration-tests/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..84c621ffe541e63fba9b315967ecad188286989c
--- /dev/null
+++ b/org.oniroproject.integration-tests/README.rst
@@ -0,0 +1 @@
+Checkbox test provider for Oniro Project
diff --git a/org.oniroproject.integration-tests/bin/alsa_pcm_info.py b/org.oniroproject.integration-tests/bin/alsa_pcm_info.py
new file mode 100755
index 0000000000000000000000000000000000000000..abb870fbc1ed9d57c917f87ad55f2fbdf4d837b9
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/alsa_pcm_info.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+# Copyright 2015 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#    Authors: Jonathan Cave <jonathan.cave@canonical.com>
+
+"""
+Script to print some simple information from the /proc/asound/pcm file. Used
+in lieu of working alsa-utils.
+"""
+
+import os
+
+PCM_FILE = '/proc/asound/pcm'
+
+if os.path.exists(PCM_FILE):
+    with open(PCM_FILE, 'r') as f:
+        for line in f:
+            t = [l.strip() for l in line.split(':')]
+            # 0 = Card and device id
+            ids = t[0].split('-')
+            print("Card: {}".format(ids[0]))
+            print("Device: {}".format(ids[1]))
+            # 1 = Name of device
+            print("Name: {}".format(t[1]))
+            # 2 = Name of device again ?!
+            # 3+ = Some number of capabilties
+            for cap in t[3:]:
+                if cap.startswith('playback'):
+                    print("Playback: 1")
+                if cap.startswith('capture'):
+                    print("Capture: 1")
+            print()
diff --git a/org.oniroproject.integration-tests/bin/bluez_list_adapters.py b/org.oniroproject.integration-tests/bin/bluez_list_adapters.py
new file mode 100755
index 0000000000000000000000000000000000000000..87c68c29f484781150af2748636e373c1a7dae06
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/bluez_list_adapters.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# This file is part of Checkbox.
+#
+# Copyright 2018 Canonical Ltd.
+#
+# Authors:
+# Jonathan Cave <jonathan.cave@canonical.com>
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+
+import checkbox_support.bt_helper as bt_helper
+
+import sys
+
+
+def main():
+    manager = bt_helper.BtManager()
+    bt_adapter_not_found = True
+    for dev in manager.get_bt_adapters():
+        bt_adapter_not_found = False
+        print(dev._if.object_path.split('/')[-1])
+    return bt_adapter_not_found
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/bt_list_adapters.py b/org.oniroproject.integration-tests/bin/bt_list_adapters.py
new file mode 100755
index 0000000000000000000000000000000000000000..c8db787e2ae0e56c92232116554395ba03c37dee
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/bt_list_adapters.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# This file is part of Checkbox.
+#
+# Copyright 2018 Canonical Ltd.
+#
+# Authors:
+#    Jonathan Cave <jonathan.cave@canonical.com>
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+
+def main():
+    rfkill = '/sys/class/rfkill'
+    found_adatper = False
+    for rfdev in os.listdir(rfkill):
+        typef = os.path.join(rfkill, rfdev, 'type')
+        type = ''
+        with open(typef, 'r') as f:
+            type = f.read().strip()
+        if type != 'bluetooth':
+            continue
+        found_adatper = True
+        namef = os.path.join(rfkill, rfdev, 'name')
+        name = ''
+        with open(namef, 'r') as f:
+            name = f.read().strip()
+        print(rfdev, name)
+    if found_adatper is False:
+        raise SystemExit('No bluetooth adatpers registered with rfkill')
+
+
+if __name__ == "__main__":
+    main()
diff --git a/org.oniroproject.integration-tests/bin/camera_test.py b/org.oniroproject.integration-tests/bin/camera_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..a7740f41be3da227b9d973327f5ce0be99d3c473
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/camera_test.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python3
+#
+# This file is part of Checkbox.
+#
+# Copyright 2008-2018 Canonical Ltd.
+# Written by:
+#   Matt Fischer <matt@mattfischer.com>
+#   Sylvain Pineau <sylvain.pineau@canonical.com>
+#
+# The v4l2 ioctl code comes from the Python bindings for the v4l2
+# userspace api (http://pypi.python.org/pypi/v4l2):
+# Copyright (C) 1999-2009 the contributors
+#
+# The JPEG metadata parser is a part of bfg-pages:
+# http://code.google.com/p/bfg-pages/source/browse/trunk/pages/getimageinfo.py
+# Copyright (C) Tim Hoffman
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import argparse
+import ctypes
+import errno
+import fcntl
+import imghdr
+import logging
+import os
+import re
+import struct
+import sys
+
+from glob import glob
+from subprocess import check_call, CalledProcessError, STDOUT
+from tempfile import NamedTemporaryFile
+
+
+_IOC_NRBITS = 8
+_IOC_TYPEBITS = 8
+_IOC_SIZEBITS = 14
+
+_IOC_NRSHIFT = 0
+_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
+_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
+_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS
+
+_IOC_WRITE = 1
+_IOC_READ = 2
+
+
+def _IOC(dir_, type_, nr, size):
+    return (
+        ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value |
+        ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value |
+        ctypes.c_int32(nr << _IOC_NRSHIFT).value |
+        ctypes.c_int32(size << _IOC_SIZESHIFT).value)
+
+
+def _IOC_TYPECHECK(t):
+    return ctypes.sizeof(t)
+
+
+def _IOR(type_, nr, size):
+    return _IOC(_IOC_READ, type_, nr, ctypes.sizeof(size))
+
+
+def _IOWR(type_, nr, size):
+    return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size))
+
+
+class v4l2_capability(ctypes.Structure):
+    """
+    Driver capabilities
+    """
+    _fields_ = [
+        ('driver', ctypes.c_char * 16),
+        ('card', ctypes.c_char * 32),
+        ('bus_info', ctypes.c_char * 32),
+        ('version', ctypes.c_uint32),
+        ('capabilities', ctypes.c_uint32),
+        ('reserved', ctypes.c_uint32 * 4),
+    ]
+
+
+# Values for 'capabilities' field
+V4L2_CAP_VIDEO_CAPTURE = 0x00000001
+V4L2_CAP_VIDEO_OVERLAY = 0x00000004
+V4L2_CAP_READWRITE = 0x01000000
+V4L2_CAP_STREAMING = 0x04000000
+
+v4l2_frmsizetypes = ctypes.c_uint
+(
+    V4L2_FRMSIZE_TYPE_DISCRETE,
+    V4L2_FRMSIZE_TYPE_CONTINUOUS,
+    V4L2_FRMSIZE_TYPE_STEPWISE,
+) = range(1, 4)
+
+
+class v4l2_frmsize_discrete(ctypes.Structure):
+    _fields_ = [
+        ('width', ctypes.c_uint32),
+        ('height', ctypes.c_uint32),
+    ]
+
+
+class v4l2_frmsize_stepwise(ctypes.Structure):
+    _fields_ = [
+        ('min_width', ctypes.c_uint32),
+        ('min_height', ctypes.c_uint32),
+        ('step_width', ctypes.c_uint32),
+        ('min_height', ctypes.c_uint32),
+        ('max_height', ctypes.c_uint32),
+        ('step_height', ctypes.c_uint32),
+    ]
+
+
+class v4l2_frmsizeenum(ctypes.Structure):
+    class _u(ctypes.Union):
+        _fields_ = [
+            ('discrete', v4l2_frmsize_discrete),
+            ('stepwise', v4l2_frmsize_stepwise),
+        ]
+
+    _fields_ = [
+        ('index', ctypes.c_uint32),
+        ('pixel_format', ctypes.c_uint32),
+        ('type', ctypes.c_uint32),
+        ('_u', _u),
+        ('reserved', ctypes.c_uint32 * 2)
+    ]
+
+    _anonymous_ = ('_u',)
+
+
+class v4l2_fmtdesc(ctypes.Structure):
+    _fields_ = [
+        ('index', ctypes.c_uint32),
+        ('type', ctypes.c_int),
+        ('flags', ctypes.c_uint32),
+        ('description', ctypes.c_char * 32),
+        ('pixelformat', ctypes.c_uint32),
+        ('reserved', ctypes.c_uint32 * 4),
+    ]
+
+
+V4L2_FMT_FLAG_COMPRESSED = 0x0001
+V4L2_FMT_FLAG_EMULATED = 0x0002
+
+
+# ioctl code for video devices
+VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability)
+VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum)
+VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc)
+
+
+class CameraTest:
+    """
+    A simple class that displays a test image via GStreamer.
+    """
+    def __init__(self, args):
+        self.args = args
+        self._width = 640
+        self._height = 480
+        self._devices = []
+
+    def detect(self):
+        """
+        Display information regarding webcam hardware
+        """
+        cap_status = dev_status = 1
+        for i in range(10):
+            cp = v4l2_capability()
+            device = '/dev/video%d' % i
+            try:
+                with open(device, 'r') as vd:
+                    fcntl.ioctl(vd, VIDIOC_QUERYCAP, cp)
+            except IOError:
+                continue
+            dev_status = 0
+            print("%s: OK" % device)
+            print("    name   : %s" % cp.card.decode('UTF-8'))
+            print("    driver : %s" % cp.driver.decode('UTF-8'))
+            print(
+                "    version: %s.%s.%s"
+                % (cp.version >> 16, (cp.version >> 8) & 0xff,
+                   cp.version & 0xff))
+            print("    flags  : 0x%x [" % cp.capabilities,
+                  ' CAPTURE' if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE
+                  else '',
+                  ' OVERLAY' if cp.capabilities & V4L2_CAP_VIDEO_OVERLAY
+                  else '',
+                  ' READWRITE' if cp.capabilities & V4L2_CAP_READWRITE
+                  else '',
+                  ' STREAMING' if cp.capabilities & V4L2_CAP_STREAMING
+                  else '',
+                  ' ]', sep="")
+
+            resolutions = self._supported_resolutions_to_string(
+                self._get_supported_resolutions(device))
+            resolutions = resolutions.replace(
+                "Resolutions:", "    Resolutions:")
+            resolutions = resolutions.replace("Format:", "    Format:")
+            print(resolutions)
+
+            if cp.capabilities & V4L2_CAP_VIDEO_CAPTURE:
+                cap_status = 0
+        return dev_status | cap_status
+
+    def _stop(self):
+        self.camerabin.set_state(Gst.State.NULL)
+        Gtk.main_quit()
+
+    def _on_error(self, bus, msg):
+        Gtk.main_quit()
+
+    def _on_destroy(self, *args):
+        Clutter.main_quit()
+
+    def _take_photo(self, filename):
+        self.camerabin.set_property("location", filename)
+        self.camerabin.emit("start-capture")
+
+    def _setup(self, sink=None):
+        webcam = Gst.ElementFactory.make('v4l2src')
+        webcam.set_property('device', self.args.device)
+        wrappercamerabinsrc = Gst.ElementFactory.make('wrappercamerabinsrc')
+        wrappercamerabinsrc.set_property('video-source', webcam)
+        self.camerabin = Gst.ElementFactory.make("camerabin")
+        self.camerabin.set_property('camera-source', wrappercamerabinsrc)
+        if sink:
+            vf_sink = Gst.ElementFactory.make(sink)
+            self.camerabin.set_property('viewfinder-sink', vf_sink)
+        self.camerabin.set_state(Gst.State.PAUSED)
+        caps = self.camerabin.get_property('viewfinder-supported-caps')
+        supported_resolutions = {}
+        for i in range(caps.get_size()):
+            key = caps.get_structure(i).get_int('width').value
+            if key not in supported_resolutions.keys():
+                supported_resolutions[key] = set()
+            supported_resolutions[key].add(
+                caps.get_structure(i).get_int('height').value)
+        if not supported_resolutions:
+            raise SystemExit("No supported resolutions found!")
+        width = min(supported_resolutions.keys(),
+                    key=lambda x: abs(x - self._width))
+        height = min(supported_resolutions[width],
+                     key=lambda y: abs(y - self._height))
+        vf_caps = Gst.Caps.from_string(
+            'video/x-raw, width={}, height={}'.format(width, height))
+        self.camerabin.set_property('viewfinder-caps', vf_caps)
+        bus = self.camerabin.get_bus()
+        bus.add_signal_watch()
+        bus.connect('message::error', self._on_error)
+        self.camerabin.set_state(Gst.State.PLAYING)
+
+    def led(self):
+        """
+        Activate camera (switch on led), but don't display any output
+        """
+        self._setup(sink='fakesink')
+        GLib.timeout_add_seconds(3, self._stop)
+        Gtk.main()
+
+    def display(self):
+        """
+        Displays the preview window
+        """
+        self._setup()
+        GLib.timeout_add_seconds(10, self._stop)
+        Gtk.main()
+
+    def still(self):
+        """
+        Captures an image to a file
+        """
+        if self.args.filename:
+            self._still_helper(self.args.filename, self._width, self._height,
+                               self.args.quiet)
+        else:
+            with NamedTemporaryFile(prefix='camera_test_', suffix='.jpg') as f:
+                self._still_helper(f.name, self._width, self._height,
+                                   self.args.quiet)
+
+    def _still_helper(self, filename, width, height, quiet, pixelformat=None):
+        """
+        Captures an image to a given filename.  width and height specify the
+        image size and quiet controls whether the image is displayed to the
+        user (quiet = True means do not display image).
+        """
+        command = ["fswebcam", "-D 1", "-S 50", "--no-banner",
+                   "-d", self.args.device,
+                   "-r", "%dx%d"
+                   % (width, height), filename]
+        use_camerabin = False
+        if pixelformat:
+            if 'MJPG' == pixelformat:  # special tweak for fswebcam
+                pixelformat = 'MJPEG'
+            command.extend(["-p", pixelformat])
+
+        try:
+            check_call(command, stdout=open(os.devnull, 'w'), stderr=STDOUT)
+        except (CalledProcessError, OSError):
+            use_camerabin = True
+        if use_camerabin:
+            self._setup(sink='fakesink')
+            GLib.timeout_add_seconds(3, self._take_photo, filename)
+            GLib.timeout_add_seconds(4, self._stop)
+            Gtk.main()
+        if not quiet:
+            stage = Clutter.Stage()
+            stage.set_title('Camera still picture test')
+            stage.set_size(width, height)
+            stage.connect('destroy', self._on_destroy)
+            Clutter.threads_add_timeout(0, 10000, self._on_destroy, None, None)
+            still_texture = Clutter.Texture.new_from_file(filename)
+            stage.add_actor(still_texture)
+            stage.show()
+            Clutter.main()
+
+    def _supported_resolutions_to_string(self, supported_resolutions):
+        """
+        Return a printable string representing a list of supported resolutions
+        """
+        ret = ""
+        for resolution in supported_resolutions:
+            ret += "Format: %s (%s)\n" % (resolution['pixelformat'],
+                                          resolution['description'])
+            ret += "Resolutions: "
+            for res in resolution['resolutions']:
+                ret += "%sx%s," % (res[0], res[1])
+            # truncate the extra comma with :-1
+            ret = ret[:-1] + "\n"
+        return ret
+
+    def resolutions(self):
+        """
+        After querying the webcam for supported formats and resolutions,
+        take multiple images using the first format returned by the driver,
+        and see if they are valid
+        """
+        resolutions = self._get_supported_resolutions(self.args.device)
+        # print supported formats and resolutions for the logs
+        print(self._supported_resolutions_to_string(resolutions))
+
+        # pick the first format, which seems to be what the driver wants for a
+        # default.  This also matches the logic that fswebcam uses to select
+        # a default format.
+        resolution = resolutions[0]
+        if resolution:
+            print("Taking multiple images using the %s format"
+                  % resolution['pixelformat'])
+            for res in resolution['resolutions']:
+                w = res[0]
+                h = res[1]
+                f = NamedTemporaryFile(prefix='camera_test_%s%sx%s' %
+                                       (resolution['pixelformat'], w, h),
+                                       suffix='.jpg', delete=False)
+                print("Taking a picture at %sx%s" % (w, h))
+                self._still_helper(f.name, w, h, True,
+                                   pixelformat=resolution['pixelformat'])
+                if self._validate_image(f.name, w, h):
+                    print("Validated image %s" % f.name)
+                    os.remove(f.name)
+                else:
+                    print("Failed to validate image %s" % f.name,
+                          file=sys.stderr)
+                    os.remove(f.name)
+                    return 1
+            return 0
+
+    def _get_pixel_formats(self, device, maxformats=5):
+        """
+        Query the camera to see what pixel formats it supports.  A list of
+        dicts is returned consisting of format and description.  The caller
+        should check whether this camera supports VIDEO_CAPTURE before
+        calling this function.
+        """
+        supported_formats = []
+        fmt = v4l2_fmtdesc()
+        fmt.index = 0
+        fmt.type = V4L2_CAP_VIDEO_CAPTURE
+        try:
+            while fmt.index < maxformats:
+                with open(device, 'r') as vd:
+                    if fcntl.ioctl(vd, VIDIOC_ENUM_FMT, fmt) == 0:
+                        pixelformat = {}
+                        # save the int type for re-use later
+                        pixelformat['pixelformat_int'] = fmt.pixelformat
+                        pixelformat['pixelformat'] = "%s%s%s%s" % \
+                            (chr(fmt.pixelformat & 0xFF),
+                             chr((fmt.pixelformat >> 8) & 0xFF),
+                             chr((fmt.pixelformat >> 16) & 0xFF),
+                             chr((fmt.pixelformat >> 24) & 0xFF))
+                        pixelformat['description'] = fmt.description.decode()
+                        supported_formats.append(pixelformat)
+                fmt.index = fmt.index + 1
+        except IOError as e:
+            # EINVAL is the ioctl's way of telling us that there are no
+            # more formats, so we ignore it
+            if e.errno != errno.EINVAL:
+                print("Unable to determine Pixel Formats, this may be a "
+                      "driver issue.")
+            return supported_formats
+        return supported_formats
+
+    def _get_supported_resolutions(self, device):
+        """
+        Query the camera for supported resolutions for a given pixel_format.
+        Data is returned in a list of dictionaries with supported pixel
+        formats as the following example shows:
+        resolution['pixelformat'] = "YUYV"
+        resolution['description'] = "(YUV 4:2:2 (YUYV))"
+        resolution['resolutions'] = [[width, height], [640, 480], [1280, 720] ]
+
+        If we are unable to gather any information from the driver, then we
+        return YUYV and 640x480 which seems to be a safe default.
+        Per the v4l2 spec the ioctl used here is experimental
+        but seems to be well supported.
+        """
+        supported_formats = self._get_pixel_formats(device)
+        if not supported_formats:
+            resolution = {}
+            resolution['description'] = "YUYV"
+            resolution['pixelformat'] = "YUYV"
+            resolution['resolutions'] = [[640, 480]]
+            supported_formats.append(resolution)
+            return supported_formats
+
+        for supported_format in supported_formats:
+            resolutions = []
+            framesize = v4l2_frmsizeenum()
+            framesize.index = 0
+            framesize.pixel_format = supported_format['pixelformat_int']
+            with open(device, 'r') as vd:
+                try:
+                    while fcntl.ioctl(vd,
+                                      VIDIOC_ENUM_FRAMESIZES,
+                                      framesize) == 0:
+                        if framesize.type == V4L2_FRMSIZE_TYPE_DISCRETE:
+                            resolutions.append([framesize.discrete.width,
+                                               framesize.discrete.height])
+                        # for continuous and stepwise, let's just use min and
+                        # max they use the same structure and only return
+                        # one result
+                        elif (framesize.type in (V4L2_FRMSIZE_TYPE_CONTINUOUS,
+                              V4L2_FRMSIZE_TYPE_STEPWISE)):
+                            resolutions.append([framesize.stepwise.min_width,
+                                                framesize.stepwise.min_height]
+                                               )
+                            resolutions.append([framesize.stepwise.max_width,
+                                                framesize.stepwise.max_height]
+                                               )
+                            break
+                        framesize.index = framesize.index + 1
+                except IOError as e:
+                    # EINVAL is the ioctl's way of telling us that there are no
+                    # more formats, so we ignore it
+                    if e.errno != errno.EINVAL:
+                        print("Unable to determine supported framesizes "
+                              "(resolutions), this may be a driver issue.")
+            supported_format['resolutions'] = resolutions
+        return supported_formats
+
+    def _validate_image(self, filename, width, height):
+        """
+        Given a filename, ensure that the image is the width and height
+        specified and is a valid image file.
+        """
+        if imghdr.what(filename) != 'jpeg':
+            return False
+
+        outw = outh = 0
+        with open(filename, mode='rb') as jpeg:
+            jpeg.seek(2)
+            b = jpeg.read(1)
+            try:
+                while (b and ord(b) != 0xDA):
+                    while (ord(b) != 0xFF):
+                        b = jpeg.read(1)
+                    while (ord(b) == 0xFF):
+                        b = jpeg.read(1)
+                    if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
+                        jpeg.seek(3, 1)
+                        h, w = struct.unpack(">HH", jpeg.read(4))
+                        break
+                    b = jpeg.read(1)
+                outw, outh = int(w), int(h)
+            except (struct.error, ValueError):
+                pass
+
+            if outw != width:
+                print("Image width does not match, was %s should be %s" %
+                      (outw, width), file=sys.stderr)
+                return False
+            if outh != height:
+                print("Image width does not match, was %s should be %s" %
+                      (outh, height), file=sys.stderr)
+                return False
+
+            return True
+
+        return True
+
+
+def parse_arguments(argv):
+    """
+    Parse command line arguments
+    """
+    parser = argparse.ArgumentParser(description="Run a camera-related test")
+    subparsers = parser.add_subparsers(dest='test',
+                                       title='test',
+                                       description='Available camera tests')
+
+    parser.add_argument('--debug', dest='log_level',
+                        action="store_const", const=logging.DEBUG,
+                        default=logging.INFO, help="Show debugging messages")
+
+    def add_device_parameter(parser):
+        group = parser.add_mutually_exclusive_group()
+        group.add_argument("-d", "--device", default="/dev/video0",
+                           help="Device for the webcam to use")
+        group.add_argument("--highest-device", action="store_true",
+                           help=("Use the /dev/videoN "
+                                 "where N is the highest value available"))
+        group.add_argument("--lowest-device", action="store_true",
+                           help=("Use the /dev/videoN "
+                                 "where N is the lowest value available"))
+    subparsers.add_parser('detect')
+    led_parser = subparsers.add_parser('led')
+    add_device_parameter(led_parser)
+    display_parser = subparsers.add_parser('display')
+    add_device_parameter(display_parser)
+    still_parser = subparsers.add_parser('still')
+    add_device_parameter(still_parser)
+    still_parser.add_argument("-f", "--filename",
+                              help="Filename to store the picture")
+    still_parser.add_argument("-q", "--quiet", action="store_true",
+                              help=("Don't display picture, "
+                                    "just write the picture to a file"))
+    resolutions_parser = subparsers.add_parser('resolutions')
+    add_device_parameter(resolutions_parser)
+    args = parser.parse_args(argv)
+
+    def get_video_devices():
+        devices = sorted(glob('/dev/video[0-9]'),
+                         key=lambda d: re.search(r'\d', d).group(0))
+        assert len(devices) > 0, "No video devices found"
+        return devices
+
+    if hasattr(args, 'highest_device') and args.highest_device:
+        args.device = get_video_devices()[-1]
+    elif hasattr(args, 'lowest_device') and args.lowest_device:
+        args.device = get_video_devices()[0]
+    return args
+
+
+if __name__ == "__main__":
+    args = parse_arguments(sys.argv[1:])
+
+    if not args.test:
+        args.test = 'detect'
+
+    logging.basicConfig(level=args.log_level)
+
+    # Import Gst only for the test cases that will need it
+    if args.test in ['display', 'still', 'led', 'resolutions']:
+        import gi
+        gi.require_version('Gst', '1.0')
+        from gi.repository import Gst
+        gi.require_version('GLib', '2.0')
+        from gi.repository import GLib
+        gi.require_version('Clutter', '1.0')
+        from gi.repository import Clutter
+        gi.require_version('Gtk', '3.0')
+        from gi.repository import Gtk
+        Gst.init(None)
+        Clutter.init()
+        Gtk.init([])
+    camera = CameraTest(args)
+    sys.exit(getattr(camera, args.test)())
diff --git a/org.oniroproject.integration-tests/bin/cpu_offlining.py b/org.oniroproject.integration-tests/bin/cpu_offlining.py
new file mode 100755
index 0000000000000000000000000000000000000000..3d269ac7d577969af5ea1db58d5954850323d221
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/cpu_offlining.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+from glob import glob
+from os.path import basename
+from math import ceil
+from time import sleep
+import sys
+
+
+def offline_cpu(cpu_name):
+    with open('/sys/devices/system/cpu/{}/online'.format(cpu_name), 'wt') as f:
+        f.write('0\n')
+
+
+def online_cpu(cpu_name):
+    with open('/sys/devices/system/cpu/{}/online'.format(cpu_name), 'wt') as f:
+        f.write('1\n')
+
+
+def is_cpu_online(cpu_name):
+    # use the same heuristic as original `cpu_offlining` test used which is to
+    # check if cpu is mentioned in /proc/interrupts
+    with open('/proc/interrupts', 'rt') as f:
+        header = f.readline().lower().split()
+        return cpu_name in header
+
+
+def main():
+    cpus = [basename(x) for x in glob('/sys/devices/system/cpu/cpu[0-9]*')]
+    # sort *numerically* cpus by their number, ignoring first 3 characters
+    # so ['cpu1', 'cpu11', 'cpu2'] is sorted to ['cpu1', 'cpu2', 'cpu11']
+    cpus.sort(key=lambda x: int(x[3:]))
+    with open('/proc/interrupts', 'rt') as f:
+        interrupts_count = len(f.readlines()) - 1  # first line is a header
+
+    # there is an arch limit on how many interrupts one cpu can handle
+    # according to LP: 1682328 it's 224. So we have to reserve some CPUs for
+    # handling them
+    max_ints_per_cpu = 224
+    reserved_cpus_count = ceil(interrupts_count / max_ints_per_cpu)
+
+    failed_offlines = []
+
+    for cpu in cpus[reserved_cpus_count:]:
+        offline_cpu(cpu)
+        sleep(0.5)
+        if is_cpu_online(cpu):
+            print("ERROR: Failed to offline {}".format(cpu), file=sys.stderr)
+            failed_offlines.append(cpu)
+
+    failed_onlines = []
+
+    for cpu in cpus[reserved_cpus_count:]:
+        online_cpu(cpu)
+        sleep(0.5)
+        if not is_cpu_online(cpu):
+            print("ERROR: Failed to online {}".format(cpu), file=sys.stderr)
+            failed_onlines.append(cpu)
+
+    if not failed_offlines and not failed_onlines:
+        print("Successfully turned {} cores off and back on".format(
+            len(cpus) - reserved_cpus_count))
+        return 0
+    else:
+        print("Error with offlining one or more cores.  CPU offline may not "
+              "work if this is an ARM system.", file=sys.stderr)
+        print(' '.join(failed_offlines))
+        print(' '.join(failed_onlines))
+        return 1
+
+
+if __name__ == '__main__':
+    main()
diff --git a/org.oniroproject.integration-tests/bin/cpu_topology.py b/org.oniroproject.integration-tests/bin/cpu_topology.py
new file mode 100755
index 0000000000000000000000000000000000000000..425e3c4130d3aae66a603b4afeda2747794c6dce
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/cpu_topology.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+'''
+cpu_topology.py
+Written by Jeffrey Lane <jeffrey.lane@canonical.com>
+'''
+import sys
+import os
+import re
+
+
+class proc_cpuinfo():
+    '''
+    Class to get and handle information from /proc/cpuinfo
+    Creates a dictionary of data gleaned from that file.
+    '''
+    def __init__(self):
+        self.cpuinfo = {}
+        cpu_fh = open('/proc/cpuinfo', 'r')
+        try:
+            temp = cpu_fh.readlines()
+        finally:
+            cpu_fh.close()
+
+        r_s390 = re.compile(r"processor [0-9]")
+        r_x86 = re.compile(r"processor\s+:")
+        for i in temp:
+            # Handle s390 first
+            if r_s390.match(i):
+                cpu_num = i.split(':')[0].split()[1].strip()
+                key = 'cpu' + cpu_num
+                self.cpuinfo[key] = {'core_id': cpu_num,
+                                     'physical_package_id': cpu_num}
+            elif r_x86.match(i):
+                key = 'cpu' + (i.split(':')[1].strip())
+                self.cpuinfo[key] = {'core_id': '', 'physical_package_id': ''}
+            elif i.startswith('core id'):
+                self.cpuinfo[key].update({'core_id': i.split(':')[1].strip()})
+            elif i.startswith('physical id'):
+                self.cpuinfo[key].update({'physical_package_id':
+                                          i.split(':')[1].strip()})
+            else:
+                continue
+
+
+class sysfs_cpu():
+    '''
+    Class to get and handle information from sysfs as relates to CPU topology
+    Creates an informational class to present information on various CPUs
+    '''
+
+    def __init__(self, proc):
+        self.syscpu = {}
+        self.path = '/sys/devices/system/cpu/' + proc + '/topology'
+        items = ['core_id', 'physical_package_id']
+        for i in items:
+            try:
+                syscpu_fh = open(os.path.join(self.path, i), 'r')
+            except OSError as e:
+                print("ERROR: %s" % e)
+                sys.exit(1)
+            else:
+                self.syscpu[i] = syscpu_fh.readline().strip()
+                syscpu_fh.close()
+
+
+def compare(proc_cpu, sys_cpu):
+    cpu_map = {}
+    '''
+    If there is only 1 CPU the test don't look for core_id
+    and physical_package_id because those information are absent in
+    /proc/cpuinfo on singlecore system
+    '''
+    for key in proc_cpu.keys():
+        if 'cpu1' not in proc_cpu:
+            cpu_map[key] = True
+        else:
+            for subkey in proc_cpu[key].keys():
+                if proc_cpu[key][subkey] == sys_cpu[key][subkey]:
+                    cpu_map[key] = True
+                else:
+                    cpu_map[key] = False
+    return cpu_map
+
+
+def main():
+    cpuinfo = proc_cpuinfo()
+    sys_cpu = {}
+    keys = cpuinfo.cpuinfo.keys()
+    for k in keys:
+        sys_cpu[k] = sysfs_cpu(k).syscpu
+    cpu_map = compare(cpuinfo.cpuinfo, sys_cpu)
+    if False in cpu_map.values() or len(cpu_map) < 1:
+        print("FAIL: CPU Topology is incorrect", file=sys.stderr)
+        print("-" * 52, file=sys.stderr)
+        print("{0}{1}".format("/proc/cpuinfo".center(30), "sysfs".center(25)),
+              file=sys.stderr)
+        print("{0}{1}{2}{3}{1}{2}".format(
+              "CPU".center(6),
+              "Physical ID".center(13),
+              "Core ID".center(9),
+              "|".center(3)), file=sys.stderr)
+        for key in sorted(sys_cpu.keys()):
+            print("{0}{1}{2}{3}{4}{5}".format(
+                  key.center(6),
+                  cpuinfo.cpuinfo[key]['physical_package_id'].center(13),
+                  cpuinfo.cpuinfo[key]['core_id'].center(9),
+                  "|".center(3),
+                  sys_cpu[key]['physical_package_id'].center(13),
+                  sys_cpu[key]['core_id'].center(9)), file=sys.stderr)
+        return 1
+    else:
+        return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/create_connection.py b/org.oniroproject.integration-tests/bin/create_connection.py
new file mode 100755
index 0000000000000000000000000000000000000000..9dc977e53cf1cdb1993f9b925333392575b95047
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/create_connection.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python3
+
+import sys
+import os
+import time
+
+from subprocess import check_call, check_output, CalledProcessError
+try:
+    from subprocess import DEVNULL  # >= python3.3
+except ImportError:
+    DEVNULL = open(os.devnull, 'wb')
+
+from uuid import uuid4
+from argparse import ArgumentParser
+
+CONNECTIONS_PATH = '/etc/NetworkManager/system-connections/'
+
+
+def wifi_connection_section(ssid, uuid):
+
+    if not uuid:
+        uuid = uuid4()
+
+    connection = """
+[connection]
+id=%s
+uuid=%s
+type=802-11-wireless
+    """ % (ssid, uuid)
+
+    wireless = """
+[802-11-wireless]
+ssid=%s
+mode=infrastructure""" % (ssid)
+
+    return connection + wireless
+
+
+def wifi_security_section(security, key):
+    # Add security field to 802-11-wireless section
+    wireless_security = """
+security=802-11-wireless-security
+
+[802-11-wireless-security]
+    """
+
+    if security.lower() == 'wpa':
+        wireless_security += """
+key-mgmt=wpa-psk
+auth-alg=open
+psk=%s
+        """ % key
+
+    elif security.lower() == 'wep':
+        wireless_security += """
+key-mgmt=none
+wep-key=%s
+        """ % key
+
+    return wireless_security
+
+
+def wifi_ip_sections():
+    ip = """
+[ipv4]
+method=auto
+
+[ipv6]
+method=auto
+    """
+
+    return ip
+
+
+def mobilebroadband_connection_section(name, uuid, connection_type):
+    if not uuid:
+        uuid = uuid4()
+
+    connection_section = """
+[connection]
+id={name}
+uuid={uuid}
+type={type}
+autoconnect=false
+    """.format(name=name, uuid=uuid, type=connection_type)
+
+    return connection_section
+
+
+def mobilebroadband_type_section(connection_type, apn,
+                                 username, password, pin):
+    number = ('*99#' if connection_type == 'gsm' else '#777')
+    type_section = """
+[{type}]
+number={number}
+""".format(type=connection_type, number=number)
+
+    if apn:
+        type_section += "\napn={apn}".format(apn=apn)
+    if username:
+        type_section += "\nusername={username}".format(username=username)
+    if password:
+        type_section += "\npassword={password}".format(password=password)
+    if pin:
+        type_section += "\npin={pin}".format(pin=pin)
+
+    return type_section
+
+
+def mobilebroadband_ppp_section():
+    return """
+[ppp]
+lcp-echo-interval=4
+lcp-echo-failure=30
+    """
+
+
+def mobilebroadband_ip_section():
+    return """
+[ipv4]
+method=auto
+    """
+
+
+def mobilebroadband_serial_section():
+    return """
+[serial]
+baud=115200
+    """
+
+
+def block_until_created(connection, retries, interval):
+    while retries > 0:
+        try:
+            nmcli_con_list = check_output(['nmcli', 'con', 'list'],
+                                          stderr=DEVNULL,
+                                          universal_newlines=True)
+        except CalledProcessError:
+            check_call(['nmcli', 'con', 'reload'])
+            nmcli_con_list = check_output(['nmcli', 'con', 'show'],
+                                          stderr=DEVNULL,
+                                          universal_newlines=True)
+
+        if connection in nmcli_con_list:
+            print("Connection %s registered" % connection)
+            break
+
+        time.sleep(interval)
+        retries = retries - 1
+
+    if retries <= 0:
+        print("Failed to register %s." % connection, file=sys.stderr)
+        sys.exit(1)
+    else:
+        try:
+            check_call(['nmcli', 'con', 'up', 'id', connection])
+            print("Connection %s activated." % connection)
+        except CalledProcessError as error:
+            print("Failed to activate %s." % connection, file=sys.stderr)
+            sys.exit(error.returncode)
+
+
+def write_connection_file(name, connection_info):
+    try:
+        connection_file = open(CONNECTIONS_PATH + name, 'w')
+        connection_file.write(connection_info)
+        os.fchmod(connection_file.fileno(), 0o600)
+        connection_file.close()
+    except IOError:
+        print("Can't write to " + CONNECTIONS_PATH + name +
+              ". Is this command being run as root?", file=sys.stderr)
+        sys.exit(1)
+
+
+def create_wifi_connection(args):
+    wifi_connection = wifi_connection_section(args.ssid, args.uuid)
+
+    if args.security:
+        # Set security options
+        if not args.key:
+            print("You need to specify a key using --key "
+                  "if using wireless security.", file=sys.stderr)
+            sys.exit(1)
+
+        wifi_connection += wifi_security_section(args.security, args.key)
+    elif args.key:
+        print("You specified an encryption key "
+              "but did not give a security type "
+              "using --security.", file=sys.stderr)
+        sys.exit(1)
+
+    try:
+        check_call(['rfkill', 'unblock', 'wlan', 'wifi'])
+    except CalledProcessError:
+        print("Could not unblock wireless "
+              "devices with rfkill.", file=sys.stderr)
+        # Don't fail the script if unblock didn't work though
+
+    wifi_connection += wifi_ip_sections()
+
+    # NetworkManager replaces forward-slashes in SSIDs with asterisks
+    name = args.ssid.replace('/', '*')
+    write_connection_file(name, wifi_connection)
+
+    return name
+
+
+def create_mobilebroadband_connection(args):
+    name = args.name
+
+    mobilebroadband_connection = mobilebroadband_connection_section(name,
+                                                                    args.uuid,
+                                                                    args.type)
+    mobilebroadband_connection += mobilebroadband_type_section(args.type,
+                                                               args.apn,
+                                                               args.username,
+                                                               args.password,
+                                                               args.pin)
+
+    if args.type == 'cdma':
+        mobilebroadband_connection += mobilebroadband_ppp_section()
+
+    mobilebroadband_connection += mobilebroadband_ip_section()
+    mobilebroadband_connection += mobilebroadband_serial_section()
+
+    write_connection_file(name, mobilebroadband_connection)
+    return name
+
+
+def main():
+    parser = ArgumentParser()
+    subparsers = parser.add_subparsers(help="sub command help")
+
+    wifi_parser = subparsers.add_parser('wifi',
+                                        help='Create a Wifi connection.')
+    wifi_parser.add_argument('ssid',
+                             help="The SSID to connect to.")
+    wifi_parser.add_argument('-S', '--security',
+                             choices=['wpa', 'wep'],
+                             help=("The type of security to be used by the "
+                                   "connection. No security will be used if "
+                                   "nothing is specified."))
+    wifi_parser.add_argument('-K', '--key',
+                             help="The encryption key required by the router.")
+    wifi_parser.set_defaults(func=create_wifi_connection)
+
+    mobilebroadband_parser = subparsers.add_parser('mobilebroadband',
+                                                   help="Create a "
+                                                        "mobile "
+                                                        "broadband "
+                                                        "connection.")
+    mobilebroadband_parser.add_argument('type',
+                                        choices=['gsm', 'cdma'],
+                                        help="The type of connection.")
+    mobilebroadband_parser.add_argument('-n', '--name',
+                                        default='MobileBB',
+                                        help="The name of the connection.")
+    mobilebroadband_parser.add_argument('-a', '--apn',
+                                        help="The APN to connect to.")
+    mobilebroadband_parser.add_argument('-u', '--username',
+                                        help="The username required by the "
+                                             "mobile broadband access point.")
+    mobilebroadband_parser.add_argument('-p', '--password',
+                                        help="The password required by the "
+                                             "mobile broadband access point.")
+    mobilebroadband_parser.add_argument('-P', '--pin',
+                                        help="The PIN of the SIM "
+                                             "card, if set.")
+    mobilebroadband_parser.set_defaults(func=create_mobilebroadband_connection)
+
+    parser.add_argument('-U', '--uuid',
+                        help="""The uuid to assign to the connection for use by
+                                NetworkManager. One will be generated if not
+                                specified here.""")
+    parser.add_argument('-R', '--retries',
+                        help="""The number of times to attempt bringing up the
+                                connection until it is confirmed as active.""",
+                        default=5)
+    parser.add_argument('-I', '--interval',
+                        help=("The time to wait between attempts to detect "
+                              "the registration of the connection."),
+                        default=2)
+    args = parser.parse_args()
+
+    # Call function to create the appropriate connection type
+    connection_name = args.func(args)
+    # Make sure we don't exit until the connection is fully created
+    block_until_created(connection_name, args.retries, args.interval)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/org.oniroproject.integration-tests/bin/eth_hotplugging.py b/org.oniroproject.integration-tests/bin/eth_hotplugging.py
new file mode 100755
index 0000000000000000000000000000000000000000..f1c910decaf66fe35ab4930009eeac5182b82a16
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/eth_hotplugging.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+# Copyright 2021 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#   Maciej Kisielewski <maciej.kisielewski@canonical.com>
+"""Check if hotplugging works on an ethernet port."""
+
+import sys
+import time
+
+
+def has_cable(iface):
+    """Check if cable is inserted in the ethernet port identified by iface."""
+    path = '/sys/class/net/{}/carrier'.format(iface)
+    with open(path) as carrier:
+        return carrier.read()[0] == '1'
+
+
+def main():
+    """Entry point to the program."""
+    if len(sys.argv) != 2:
+        raise SystemExit("Usage {} INTERFACE_NAME".format(sys.argv[0]))
+    iface = sys.argv[1]
+    # sanity check of the interface path
+    try:
+        has_cable(iface)
+    except Exception as exc:
+        msg = "Could not check the cable for '{}': {}".format(iface, exc)
+        raise SystemExit(msg) from exc
+    print(("Press enter and unplug the ethernet cable "
+           "from the port {} of the System.").format(iface))
+    print("After 15 seconds plug it back in.")
+    print("Checkbox session may be interrupted but it should come back up.")
+    input()
+    print("Waiting for cable to get disconnected.")
+    elapsed = 0
+    while elapsed < 60:
+        if not has_cable(sys.argv[1]):
+            break
+        time.sleep(1)
+        print(".", flush=True, end='')
+        elapsed += 1
+    else:
+        raise SystemExit("Failed to detect unplugging!")
+    print("Cable unplugged!")
+    print("Waiting for the cable to get connected.")
+    elapsed = 0
+    while elapsed < 60:
+        if has_cable(sys.argv[1]):
+            break
+        time.sleep(1)
+        print(".", flush=True, end='')
+        elapsed += 1
+    else:
+        raise SystemExit("Failed to detect plugging it back!")
+    print("Cable detected!")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/org.oniroproject.integration-tests/bin/gateway_ping_test.py b/org.oniroproject.integration-tests/bin/gateway_ping_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..59f43d31ce34ccf891c82e27e6c96c9a5275e7a4
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/gateway_ping_test.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+# This file is part of Checkbox.
+#
+# Copyright 2007-2014 Canonical Ltd.
+# Written by:
+#   Brendan Donegan <brendan.donegan@canonical.com>
+#   Daniel Manrique <daniel.manrique@canonical.com>
+#   David Murphy <david.murphy@canonical.com>
+#   Javier Collado <javier.collado@canonical.com>
+#   Jeff Lane <jeffrey.lane@canonical.com>
+#   Marc Tardif <marc.tardif@canonical.com>
+#   Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com>
+#   Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+
+from gettext import gettext as _
+import argparse
+import errno
+import gettext
+import logging
+import os
+import re
+import socket
+import struct
+import subprocess
+import sys
+import time
+
+
+class Route:
+    """
+    Gets routing information from the system.
+    """
+
+    def _num_to_dotted_quad(self, number):
+        """
+        Convert long int to dotted quad string
+        """
+        return socket.inet_ntoa(struct.pack("<L", number))
+
+    def _get_default_gateway_from_proc(self):
+        """
+        Returns the current default gateway, reading that from /proc
+        """
+        logging.debug(_("Reading default gateway information from /proc"))
+        try:
+            with open("/proc/net/route", "rt") as stream:
+                route = stream.read()
+        except Exception:
+            logging.error(_("Failed to read def gateway from /proc"))
+            return None
+        else:
+            h = re.compile(r"\n(?P<interface>\w+)\s+00000000\s+"
+                           r"(?P<def_gateway>[\w]+)\s+")
+            w = h.search(route)
+            if w:
+                if w.group("def_gateway"):
+                    return self._num_to_dotted_quad(
+                        int(w.group("def_gateway"), 16))
+                else:
+                    logging.error(
+                        _("Could not find def gateway info in /proc"))
+                    return None
+            else:
+                logging.error(_("Could not find def gateway info in /proc"))
+                return None
+
+    def _get_default_gateway_from_bin_route(self):
+        """
+        Get default gateway from /sbin/route -n
+        Called by get_default_gateway
+        and is only used if could not get that from /proc
+        """
+        logging.debug(
+            _("Reading default gateway information from route binary"))
+        routebin = subprocess.getstatusoutput(
+            "export LANGUAGE=C; " "/usr/bin/env route -n")
+        if routebin[0] == 0:
+            h = re.compile(r"\n0.0.0.0\s+(?P<def_gateway>[\w.]+)\s+")
+            w = h.search(routebin[1])
+            if w:
+                def_gateway = w.group("def_gateway")
+                if def_gateway:
+                    return def_gateway
+        logging.error(_("Could not find default gateway by running route"))
+        return None
+
+    def get_hostname(self):
+        return socket.gethostname()
+
+    def get_default_gateway(self):
+        t1 = self._get_default_gateway_from_proc()
+        if not t1:
+            t1 = self._get_default_gateway_from_bin_route()
+        return t1
+
+
+def get_host_to_ping(interface=None, verbose=False, default=None):
+    # Get list of all IPs from all my interfaces,
+    interface_list = subprocess.check_output(["ip", "-o", 'addr', 'show'])
+    reg = re.compile(r'\d: (?P<iface>\w+) +inet (?P<address>[\d\.]+)/'
+                     r'(?P<netmask>[\d]+) brd (?P<broadcast>[\d\.]+)')
+    # Will magically exclude lo because it lacks brd field
+    interfaces = reg.findall(interface_list.decode())
+    # ping -b the network on each one (one ping only)
+    # exclude the ones not specified in iface
+    for iface in interfaces:
+        if not interface or iface[0] == interface:
+            # Use check_output even if I'll discard the output
+            # looks cleaner than using .call and redirecting stdout to null
+            try:
+                subprocess.check_output(["ping", "-q", "-c", "1", "-b",
+                                         iface[3]], stderr=subprocess.STDOUT)
+            except subprocess.CalledProcessError:
+                pass
+    # If default host given, ping it as well,
+    # to try to get it into the arp table.
+    # Needed in case it's not responding to broadcasts.
+    if default:
+        try:
+            subprocess.check_output(["ping", "-q", "-c", "1", default],
+                                    stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError:
+            pass
+    # Try to get the gateway address for the interface from networkctl
+    cmd = 'networkctl status --no-pager --no-legend {}'.format(interface)
+    try:
+        output = subprocess.check_output(cmd, shell=True)
+        for line in output.decode(sys.stdout.encoding).splitlines():
+            vals = line.strip().split(' ')
+            if len(vals) >= 2:
+                if vals[0] == 'Gateway:':
+                    subprocess.check_output(["ping", "-q", "-c", "1", vals[1]],
+                                            stderr=subprocess.STDOUT)
+                    break
+    except subprocess.CalledProcessError:
+        pass
+    ARP_POPULATE_TRIES = 10
+    num_tries = 0
+    while num_tries < ARP_POPULATE_TRIES:
+        # Get output from arp -a -n to get known IPs
+        known_ips = subprocess.check_output(["arp", "-a", "-n"])
+        reg = re.compile(r'\? \((?P<ip>[\d.]+)\) at (?P<mac>[a-f0-9\:]+) '
+                         r'\[ether\] on (?P<iface>[\w\d]+)')
+        # Filter (if needed) IPs not on the specified interface
+        pingable_ips = [pingable[0] for pingable in reg.findall(
+                        known_ips.decode()) if not interface or
+                        pingable[2] == interface]
+        # If the default given ip is among the remaining ones,
+        # ping that.
+        if default and default in pingable_ips:
+            if verbose:
+                print(_(
+                    "Desired ip address {0} is reachable, using it"
+                ).format(default))
+            return default
+        # If not, choose another IP.
+        address_to_ping = pingable_ips[0] if len(pingable_ips) else None
+        if verbose:
+            print(_(
+                "Desired ip address {0} is not reachable from {1},"
+                " using {2} instead"
+            ).format(default, interface, address_to_ping))
+        if address_to_ping:
+            return address_to_ping
+        time.sleep(2)
+        num_tries += 1
+    # Wait time expired
+    return None
+
+
+def ping(host, interface, count, deadline, verbose=False):
+    command = ["ping", str(host),  "-c", str(count), "-w", str(deadline)]
+    if interface:
+        command.append("-I{}".format(interface))
+    reg = re.compile(
+        r"(\d+) packets transmitted, (\d+) received,"
+        r".*([0-9]*\.?[0-9]*.)% packet loss")
+    ping_summary = {'transmitted': 0, 'received': 0, 'pct_loss': 0}
+    try:
+        output = subprocess.check_output(
+            command, universal_newlines=True, stderr=subprocess.PIPE)
+    except OSError as exc:
+        if exc.errno == errno.ENOENT:
+            # No ping command present;
+            # default exception message is informative enough.
+            print(exc)
+        else:
+            raise
+    except subprocess.CalledProcessError as excp:
+        # Ping returned fail exit code
+        print(_("ERROR: ping result: {0}").format(excp))
+        if excp.stderr:
+            print(excp.stderr)
+            if 'SO_BINDTODEVICE' in excp.stderr:
+                ping_summary['cause'] = (
+                    "Could not bind to the {} interface.".format(interface))
+    else:
+        if verbose:
+            print(output)
+        received = re.findall(reg, output)
+        if received:
+            ping_summary = received[0]
+            ping_summary = {
+                'transmitted': int(ping_summary[0]),
+                'received': int(ping_summary[1]),
+                'pct_loss': int(ping_summary[2])}
+    return ping_summary
+
+
+def main(args):
+    gettext.textdomain("com.canonical.certification.checkbox")
+    gettext.bindtextdomain("com.canonical.certification.checkbox",
+                           os.getenv("CHECKBOX_PROVIDER_LOCALE_DIR", None))
+    default_count = 2
+    default_delay = 4
+    route = Route()
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "host", nargs='?', default=route.get_default_gateway(),
+        help=_("host to ping"))
+    parser.add_argument(
+        "-c", "--count", default=default_count, type=int,
+        help=_("number of packets to send"))
+    parser.add_argument(
+        "-d", "--deadline", default=default_delay, type=int,
+        help=_("timeout in seconds"))
+    parser.add_argument(
+        "-t", "--threshold", default=0, type=int,
+        help=_("allowed packet loss percentage (default: %(default)s)"))
+    parser.add_argument(
+        "-v", "--verbose", action='store_true', help=_("be verbose"))
+    parser.add_argument(
+        "-I", "--interface", help=_("use specified interface to send packets"))
+    args = parser.parse_args()
+    # Ensure count and deadline make sense. Adjust them if not.
+    if args.deadline != default_delay and args.count != default_count:
+        # Ensure they're both consistent, and exit with a warning if not,
+        # rather than modifying what the user explicitly set.
+        if args.deadline <= args.count:
+            # FIXME: this cannot ever be translated correctly
+            print(_(
+                "ERROR: not enough time for {0} pings in {1} seconds"
+            ).format(args.count, args.deadline))
+            return 1
+    elif args.deadline != default_delay:
+        # Adjust count according to delay.
+        args.count = args.deadline - 1
+        if args.count < 1:
+            args.count = 1
+        if args.verbose:
+            # FIXME: this cannot ever be translated correctly
+            print(_(
+                "Adjusting ping count to {0} to fit in {1}-second deadline"
+            ).format(args.count, args.deadline))
+    else:
+        # Adjust delay according to count
+        args.deadline = args.count + 1
+        if args.verbose:
+            # FIXME: this cannot ever be translated correctly
+            print(_(
+                "Adjusting deadline to {0} seconds to fit {1} pings"
+            ).format(args.deadline, args.count))
+    # If given host is not pingable, override with something pingable.
+    host = get_host_to_ping(
+        interface=args.interface, verbose=args.verbose, default=args.host)
+    if args.verbose:
+        print(_("Checking connectivity to {0}").format(host))
+    ping_summary = None
+    if host:
+        ping_summary = ping(host, args.interface, args.count,
+                            args.deadline, args.verbose)
+    if ping_summary is None or ping_summary['received'] == 0:
+        print(_("No Internet connection"))
+        if ping_summary.get('cause'):
+            print("Possible cause: {}".format(ping_summary['cause']))
+        return 1
+    elif ping_summary['transmitted'] != ping_summary['received']:
+        print(_("Connection established, but lost {0}% of packets").format(
+            ping_summary['pct_loss']))
+        if ping_summary['pct_loss'] > args.threshold:
+            print(_(
+                "FAIL: {0}% packet loss is higher than {1}% threshold"
+            ).format(ping_summary['pct_loss'], args.threshold))
+            return 1
+        else:
+            print(_(
+                "PASS: {0}% packet loss is within {1}% threshold"
+            ).format(ping_summary['pct_loss'], args.threshold))
+            return 0
+    else:
+        print(_("Connection to test host fully established"))
+        return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/org.oniroproject.integration-tests/bin/gpio_gpiomem_loopback.py b/org.oniroproject.integration-tests/bin/gpio_gpiomem_loopback.py
new file mode 100755
index 0000000000000000000000000000000000000000..c52cb2360bf1f045a5c7f47bef0d48ca3fd27d80
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/gpio_gpiomem_loopback.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# Copyright 2019 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#   Jonathan Cave <jonathan.cave@canonical.com>
+
+import RPi.GPIO as GPIO
+
+import os
+import sys
+import time
+
+
+def loopback_test(out_lane, in_lane):
+    print("{} -> {}".format(out_lane, in_lane), flush=True)
+    out_lane = int(out_lane)
+    in_lane = int(in_lane)
+    GPIO.setup(out_lane, GPIO.OUT, initial=GPIO.LOW)
+    GPIO.setup(in_lane, GPIO.IN)
+    for i in range(6):
+        GPIO.output(out_lane, i % 2)
+        time.sleep(0.5)
+        if GPIO.input(in_lane) != (i % 2):
+            raise SystemExit("Failed loopback test out: {} in: {}".format(
+                out_lane, in_lane))
+        time.sleep(0.5)
+
+
+def gpio_pairs(model_name):
+    gpio_data = os.path.expandvars(
+        '$PLAINBOX_PROVIDER_DATA/gpio-loopback.{}.in'.format(model_name))
+    if not os.path.exists(gpio_data):
+        raise SystemExit(
+            "ERROR: no gpio information found at: {}".format(gpio_data))
+    with open(gpio_data, 'r') as f:
+        for line in f:
+            if line.startswith('#'):
+                continue
+            yield line.strip().split(',')
+
+
+def main():
+    if len(sys.argv) < 2:
+        raise SystemExit('Usage: gpio_loopback.py MODEL_NAME')
+    model_name = sys.argv[1]
+
+    print("Using RPi.GPIO module {}".format(GPIO.VERSION))
+    GPIO.setmode(GPIO.BCM)
+    GPIO.setwarnings(False)
+
+    for pair in gpio_pairs(model_name):
+        loopback_test(*pair)
+
+    GPIO.cleanup()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/org.oniroproject.integration-tests/bin/gpio_sysfs_loopback.py b/org.oniroproject.integration-tests/bin/gpio_sysfs_loopback.py
new file mode 100755
index 0000000000000000000000000000000000000000..21d8fb2b50777bdecb3b044b5ecca893217da3ad
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/gpio_sysfs_loopback.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Copyright 2019 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#   Jonathan Cave <jonathan.cave@canonical.com>
+
+import errno
+import os
+import sys
+import time
+
+
+def export_gpio(lane):
+    try:
+        with open('/sys/class/gpio/export', 'w') as f_export:
+            f_export.write('{}\n'.format(lane))
+    except OSError as e:
+        if e.errno == errno.EBUSY:
+            # EBUSY indicates GPIO already exported
+            print('GPIO {} already exported'.format(lane))
+            pass
+        else:
+            sys.stderr.write('Failed request to export GPIO {}\n'.format(lane))
+            raise
+    # test directory exists
+    if not os.path.exists('/sys/class/gpio/gpio{}'.format(lane)):
+        raise SystemExit('GPIO {} failed to export'.format(lane))
+
+
+def unexport_gpio(lane):
+    try:
+        with open('/sys/class/gpio/unexport', 'w') as f_unexport:
+            f_unexport.write('{}\n'.format(lane))
+    except OSError:
+        sys.stderr.write('Failed request to unexport GPIO {}\n'.format(lane))
+        raise
+    # test directory removed
+    if os.path.exists('/sys/class/gpio/gpio{}'.format(lane)):
+        raise SystemExit('GPIO {} failed to export'.format(lane))
+
+
+def configure_gpio(lane, direction):
+    with open('/sys/class/gpio/gpio{}/direction'.format(lane), 'wt') as f:
+        f.write('{}\n'.format(direction))
+
+
+def write_gpio(lane, val):
+    with open('/sys/class/gpio/gpio{}/value'.format(lane), 'wt') as f:
+        f.write('{}\n'.format(val))
+
+
+def read_gpio(lane):
+    with open('/sys/class/gpio/gpio{}/value'.format(lane), 'r') as f:
+        return f.read().strip()
+
+
+def loopback_test(out_lane, in_lane):
+    print("{} -> {}".format(out_lane, in_lane), flush=True)
+    export_gpio(out_lane)
+    configure_gpio(out_lane, 'out')
+    export_gpio(in_lane)
+    configure_gpio(in_lane, 'in')
+    for i in range(6):
+        write_gpio(out_lane, i % 2)
+        time.sleep(0.5)
+        if read_gpio(in_lane) != str(i % 2):
+            raise SystemExit("Failed loopback test out: {} in: {}".format(
+                out_lane, in_lane))
+        time.sleep(0.5)
+    unexport_gpio(out_lane)
+    unexport_gpio(in_lane)
+
+
+def gpio_pairs(model_name):
+    gpio_data = os.path.expandvars(
+        '$PLAINBOX_PROVIDER_DATA/gpio-loopback.{}.in'.format(model_name))
+    if not os.path.exists(gpio_data):
+        raise SystemExit(
+            "ERROR: no gpio information found at: {}".format(gpio_data))
+    with open(gpio_data, 'r') as f:
+        for line in f:
+            if line.startswith('#'):
+                continue
+            yield line.strip().split(',')
+
+
+def main():
+    if len(sys.argv) < 2:
+        raise SystemExit('Usage: gpio_syfs_loopback.py MODEL_NAME')
+    model_name = sys.argv[1]
+    for pair in gpio_pairs(model_name):
+        loopback_test(*pair)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/org.oniroproject.integration-tests/bin/graphics_driver.py b/org.oniroproject.integration-tests/bin/graphics_driver.py
new file mode 100755
index 0000000000000000000000000000000000000000..52ca157caec0abac2ba7836255389906baa3cfc7
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/graphics_driver.py
@@ -0,0 +1,399 @@
+#!/usr/bin/env python3
+# ========================================================================
+#
+# based on xlogparse
+#
+# DESCRIPTION
+#
+# Parses Xlog.*.log format files and allows looking up data from it
+#
+# AUTHOR
+#   Bryce W. Harrington <bryce@canonical.com>
+#
+# COPYRIGHT
+#   Copyright (C) 2010-2012 Bryce W. Harrington
+#   All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# ========================================================================
+import re
+import sys
+import os
+import glob
+
+from subprocess import Popen, PIPE, check_output, CalledProcessError
+
+
+class XorgLog(object):
+
+    def __init__(self, logfile=None):
+        self.modules = []
+        self.errors = []
+        self.warnings = []
+        self.info = []
+        self.notimpl = []
+        self.notices = []
+        self.cards = []
+        self.displays = {}
+        self.xserver_version = None
+        self.boot_time = None
+        self.boot_logfile = None
+        self.kernel_version = None
+        self.video_driver = None
+        self.xorg_conf_path = None
+        self.logfile = logfile
+
+        if logfile:
+            self.parse(logfile)
+
+    def parse(self, filename):
+        self.displays = {}
+        display = {}
+        display_name = "Unknown"
+        in_file = open(filename, errors='ignore')
+        gathering_module = False
+        found_ddx = False
+        module = None
+        for line in in_file.readlines():
+
+            # Errors and Warnings
+            m = re.search(r'\(WW\) (.*)$', line)
+            if m:
+                self.warnings.append(m.group(1))
+                continue
+
+            m = re.search(r'\(EE\) (.*)$', line)
+            if m:
+                self.errors.append(m.group(1))
+                continue
+
+            # General details
+            m = re.search(r'Current Operating System: (.*)$', line)
+            if m:
+                uname = m.group(1)
+                self.kernel_version = uname.split()[2]
+                continue
+
+            m = re.search(r'Kernel command line: (.*)$', line)
+            if m:
+                self.kernel_command_line = m.group(1)
+                continue
+
+            m = re.search(r'Build Date: (.*)$', line)
+            if m:
+                self.kernel_command_line = m.group(1)
+                continue
+
+            m = re.search(r'Log file: "(.*)", Time: (.*)$', line)
+            if m:
+                self.boot_logfile = m.group(1)
+                self.boot_time = m.group(2)
+
+            m = re.search(r'xorg-server ([^ ]+) .*$', line)
+            if m:
+                self.xserver_version = m.group(1)
+                continue
+
+            m = re.search(r'Using a default monitor configuration.', line)
+            if m and self.xorg_conf_path is None:
+                self.xorg_conf_path = 'default'
+                continue
+
+            m = re.search(r'Using config file: "(.*)"', line)
+            if m:
+                self.xorg_conf_path = m.group(1)
+                continue
+
+            # Driver related information
+            m = re.search(r'\(..\)', line)
+            if m:
+                if gathering_module and module is not None:
+                    self.modules.append(module)
+                gathering_module = False
+                module = None
+                m = re.search(
+                    r'\(II\) Loading.*modules\/drivers\/(.+)_drv\.so', line)
+                if m:
+                    found_ddx = True
+                    continue
+                m = re.search(r'\(II\) Module (\w+):', line)
+                if m:
+                    module = {
+                        'name':         m.group(1),
+                        'vendor':       None,
+                        'version':      None,
+                        'class':        None,
+                        'abi_name':     None,
+                        'abi_version':  None,
+                        'ddx':          found_ddx,
+                        }
+                    found_ddx = False
+                    gathering_module = True
+
+            if gathering_module:
+                m = re.search(r'vendor="(.*:?)"', line)
+                if m:
+                    module['vendor'] = m.group(1)
+
+                m = re.search(r'module version = (.*)', line)
+                if m:
+                    module['version'] = m.group(1)
+
+                if module['name'] == 'nvidia':
+                    try:
+                        version = check_output(
+                            "nvidia-settings -v",
+                            shell=True,
+                            universal_newlines=True)
+                        m = re.search(r'.*version\s+([0-9\.]+).*', version)
+                        if m:
+                            module['version'] = m.group(1)
+                    except CalledProcessError:
+                        pass
+
+                m = re.search(r'class: (.*)', line)
+                if m:
+                    module['class'] = m.group(1)
+
+                m = re.search(r'ABI class:\s+(.*:?), version\s+(.*:?)', line)
+                if m:
+                    if m.group(1)[:5] == "X.Org":
+                        module['abi_name'] = m.group(1)[6:]
+                    else:
+                        module['abi_name'] = m.group(1)
+                    module['abi_version'] = m.group(2)
+                continue
+
+            # EDID and Modelines
+            # We use this part to determine which driver is in use
+            # For Intel / RADEON / Matrox (using modesetting)
+            m = re.search(r'\(II\) (.*)\(\d+\): EDID for output (.*)', line)
+            if m:
+                self.displays[display_name] = display
+                if m.group(1) == "modeset":
+                    self.video_driver = "modesetting"
+                else:
+                    self.video_driver = m.group(1)
+                display_name = m.group(2)
+                display = {'Output': display_name}
+                continue
+
+            m = re.search(
+                r'\(II\) (.*)\(\d+\): Assigned Display Device: (.*)$', line)
+            if m:
+                self.displays[display_name] = display
+                self.video_driver = m.group(1)
+                display_name = m.group(2)
+                display = {'Output': display_name}
+                continue
+
+            # For NVIDIA
+            m = re.search(r'\(II\) (.*)\(\d+\): Setting mode "(.*?):', line)
+            if not m:
+                m = re.search(
+                    r'\(II\) (.*)\(\d+\): Setting mode "(NULL)"', line)
+            if m:
+                self.displays[display_name] = display
+                self.video_driver = m.group(1)
+                display_name = m.group(2)
+                display = {'Output': display_name}
+                continue
+
+            # For 4th Intel after 3.11
+            m = re.search(
+                r'\(II\) (.*)\(\d+\): switch to mode .* using (.*),', line)
+            if m:
+                self.displays[display_name] = display
+                self.video_driver = 'intel'  # 'intel' is what we expect to see
+                display_name = m.group(2)
+                display = {'Output': display_name}
+                continue
+
+            m = re.search(
+                r'Manufacturer: (.*) *Model: (.*) *Serial#: (.*)', line)
+            if m:
+                display['display manufacturer'] = m.group(1)
+                display['display model'] = m.group(2)
+                display['display serial no.'] = m.group(3)
+
+            m = re.search(r'EDID Version: (.*)', line)
+            if m:
+                display['display edid version'] = m.group(1)
+
+            m = re.search(r'EDID vendor \"(.*)\", prod id (.*)', line)
+            if m:
+                display['vendor'] = m.group(1)
+                display['product id'] = m.group(2)
+
+            m = re.search(
+                r'Max Image Size \[(.*)\]: *horiz.: (.*) *vert.: (.*)', line)
+            if m:
+                display['size max horizontal'] = "%s %s" % (
+                    m.group(2), m.group(1))
+                display['size max vertical'] = "%s %s" % (
+                    m.group(3), m.group(1))
+
+            m = re.search(r'Image Size: *(.*) x (.*) (.*)', line)
+            if m:
+                display['size horizontal'] = "%s %s" % (m.group(1), m.group(3))
+                display['size vertical'] = "%s %s" % (m.group(2), m.group(3))
+
+            m = re.search(r'(.*) is preferred mode', line)
+            if m:
+                display['mode preferred'] = m.group(1)
+
+            m = re.search(r'Modeline \"(\d+)x(\d+)\"x([0-9\.]+) *(.*)$', line)
+            if m:
+                key = "mode %sx%s@%s" % (m.group(1), m.group(2), m.group(3))
+                display[key] = m.group(4)
+                continue
+
+        if display_name not in self.displays.keys():
+            self.displays[display_name] = display
+        in_file.close()
+
+    def errors_filtered(self):
+        excludes = set([
+            'error, (NI) not implemented, (??) unknown.',
+            'Failed to load module "fglrx" (module does not exist, 0)',
+            'Failed to load module "nv" (module does not exist, 0)',
+            ])
+        return [err for err in self.errors if err not in excludes]
+
+    def warnings_filtered(self):
+        excludes = set([
+            'warning, (EE) error, (NI) not implemented, (??) unknown.',
+            'The directory "/usr/share/fonts/X11/cyrillic" does not exist.',
+            'The directory "/usr/share/fonts/X11/100dpi/" does not exist.',
+            'The directory "/usr/share/fonts/X11/75dpi/" does not exist.',
+            'The directory "/usr/share/fonts/X11/100dpi" does not exist.',
+            'The directory "/usr/share/fonts/X11/75dpi" does not exist.',
+            'Warning, couldn\'t open module nv',
+            'Warning, couldn\'t open module fglrx',
+            'Falling back to old probe method for vesa',
+            'Falling back to old probe method for fbdev',
+            ])
+        return [err for err in self.warnings if err not in excludes]
+
+
+def get_driver_info(xlog):
+    '''Return the running driver and version'''
+    print('-' * 13, 'VIDEO DRIVER INFORMATION', '-' * 13)
+    if xlog.video_driver:
+        for module in xlog.modules:
+            if module['name'] == xlog.video_driver.lower():
+                print("Video Driver: %s" % module['name'])
+                print("Driver Version: %s" % module['version'])
+                print('\n')
+                return 0
+    else:
+        print("ERROR: No video driver loaded! Possibly in failsafe mode!",
+              file=sys.stderr)
+        return 1
+
+
+def is_laptop():
+    return os.path.isdir('/proc/acpi/button/lid')
+
+
+def hybrid_graphics_check(xlog):
+    '''Check for Hybrid Graphics'''
+    card_id1 = re.compile(r'.*0300: *(.+):(.+) \(.+\)')
+    card_id2 = re.compile(r'.*03..: *(.+):(.+)')
+    cards_dict = {'8086': 'Intel', '10de': 'NVIDIA', '1002': 'AMD'}
+    cards = []
+    drivers = []
+    formatted_cards = []
+
+    output = Popen(['lspci', '-n'], stdout=PIPE, universal_newlines=True)
+    card_list = output.communicate()[0].split('\n')
+
+    # List of discovered cards
+    for line in card_list:
+        m1 = card_id1.match(line)
+        m2 = card_id2.match(line)
+        if m1:
+            id1 = m1.group(1).strip().lower()
+            id2 = m1.group(2).strip().lower()
+            id = id1 + ':' + id2
+            cards.append(id)
+        elif m2:
+            id1 = m2.group(1).strip().lower()
+            id2 = m2.group(2).strip().lower()
+            id = id1 + ':' + id2
+            cards.append(id)
+
+    print('-' * 13, 'HYBRID GRAPHICS CHECK', '-' * 16)
+    for card in cards:
+        formatted_name = cards_dict.get(card.split(':')[0], 'Unknown')
+        formatted_cards.append(formatted_name)
+        print('Graphics Chipset: %s (%s)' % (formatted_name, card))
+
+    for module in xlog.modules:
+        if module['ddx'] and module['name'] not in drivers:
+            drivers.append(module['name'])
+    print('Loaded DDX Drivers: %s' % ', '.join(drivers))
+
+    has_hybrid_graphics = (len(cards) > 1 and is_laptop() and
+                           (cards_dict.get('8086') in formatted_cards or
+                            cards_dict.get('1002') in formatted_cards))
+
+    print('Hybrid Graphics: %s' % (has_hybrid_graphics and
+                                   'yes' or 'no'))
+
+    return 0
+
+
+def main():
+    usr_xorg_dir = os.path.expanduser("~/.local/share/xorg/")
+    root_xorg_dir = "/var/log/"
+    xlog = None
+    xorg_owner = []
+    tgt_dir = ""
+
+    # Output the Xorg owner
+    xorg_owner = check_output("ps -o user= -p $(pidof Xorg)",
+                              shell=True,
+                              universal_newlines=True).split()
+
+    # Check the Xorg owner and then judge the Xorg log location
+    if "root" in xorg_owner:
+        tgt_dir = root_xorg_dir
+    elif xorg_owner:
+        tgt_dir = usr_xorg_dir
+    else:
+        print("ERROR: No Xorg process found!", file=sys.stderr)
+
+    if tgt_dir:
+        xorg_file_paths = list(glob.iglob(tgt_dir + 'Xorg.*.log'))
+        target_file = xorg_file_paths[0]
+        xlog = XorgLog(target_file)
+
+    results = []
+
+    results.append(get_driver_info(xlog))
+    results.append(hybrid_graphics_check(xlog))
+
+    return 1 if 1 in results else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/i2c_driver_test.py b/org.oniroproject.integration-tests/bin/i2c_driver_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..c6aae78e31682323911a6ff35b55bb4d63566e82
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/i2c_driver_test.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+# Copyright 2016-2020 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#    Authors: Gavin Lin <gavin.lin@canonical.com>
+#             Sylvain Pineau <sylvain.pineau@canonical.com>
+#             Jonathan Cave <jonathan.cave@canonical.com>
+
+"""
+This script will check number of detected I2C buses or devices
+
+To see how to use, please run "./i2c_driver_test.py"
+"""
+
+import argparse
+import os
+import subprocess
+
+
+class Bus():
+
+    """Detect I2C bus."""
+
+    def invoked(self, args):
+        """Method called when the command is invoked."""
+        # Detect I2C buses and calculate number of them
+        result = subprocess.check_output(['i2cdetect', '-l'],
+                                         universal_newlines=True)
+        print(result)
+        bus_number = len(result.splitlines())
+        print('Detected bus number: {}'.format(bus_number))
+
+        # Test failed if no I2C bus detected
+        if bus_number == 0:
+            raise SystemExit('Test failed, no bus detected.')
+
+        # Verify if detected number of buses is as expected
+        else:
+            if args.bus != 0:
+                if bus_number == args.bus:
+                    print('Test passed')
+                else:
+                    raise SystemExit('Test failed, expecting {} I2C '
+                                     'buses.'.format(args.bus))
+
+
+class Device():
+
+    """Detect I2C device."""
+
+    def invoked(self, args):
+        # Make sure that we have root privileges
+        if os.geteuid() != 0:
+            raise SystemExit('Error: please run this command as root')
+        # Calculate number of buses
+        result = subprocess.check_output(['i2cdetect', '-l'],
+                                         universal_newlines=True)
+        detected_i2c_bus = []
+        for line in result.splitlines():
+            detected_i2c_bus.append(line.split('\t')[0].split('-')[1])
+        print('Detected buses: {}'.format(detected_i2c_bus))
+
+        # Detect device on each bus
+        exit_code = 1
+        for i in detected_i2c_bus:
+            print('Checking I2C bus {}'.format(i))
+            result = subprocess.check_output(['i2cdetect', '-y', '-r', str(i)],
+                                             universal_newlines=True)
+            print(result)
+            result_line = result.splitlines()[1:]
+            for l in result_line:
+                address_value = l.strip('\n').split(':')[1].split()
+                for v in address_value:
+                    if v != '--':
+                        exit_code = 0
+        if exit_code == 1:
+            raise SystemExit('No I2C device detected on any I2C bus')
+        print('I2C device detected')
+
+
+class I2cDriverTest():
+
+    """I2C driver test."""
+
+    def main(self):
+        subcommands = {
+            'bus': Bus,
+            'device': Device
+        }
+        parser = argparse.ArgumentParser()
+        parser.add_argument('subcommand', type=str, choices=subcommands)
+        parser.add_argument('-b', '--bus', type=int, default=0,
+                            help='Expected number of I2C bus.')
+        args = parser.parse_args()
+        subcommands[args.subcommand]().invoked(args)
+
+
+if __name__ == '__main__':
+    I2cDriverTest().main()
diff --git a/org.oniroproject.integration-tests/bin/keyboard_test.py b/org.oniroproject.integration-tests/bin/keyboard_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..e87dc2eae10e26a1cdeff56ae1aa5a2ed30a0376
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/keyboard_test.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+from gettext import gettext as _
+import gettext
+
+
+def cli_prompt():
+    import termios
+
+    limit = 50
+    separator = ord("\n")
+    fileno = sys.stdin.fileno()
+    saved_attributes = termios.tcgetattr(fileno)
+    attributes = termios.tcgetattr(fileno)
+    attributes[3] = attributes[3] & ~(termios.ICANON)
+    attributes[6][termios.VMIN] = 1
+    attributes[6][termios.VTIME] = 0
+    termios.tcsetattr(fileno, termios.TCSANOW, attributes)
+
+    sys.stdout.write(_("Enter text:\n"))
+
+    input = ""
+    try:
+        while len(input) < limit:
+            ch = str(sys.stdin.read(1))
+            if ord(ch) == separator:
+                break
+            input += ch
+    finally:
+        termios.tcsetattr(fileno, termios.TCSANOW, saved_attributes)
+
+
+def gtk_prompt():
+    import gi
+    gi.require_version('Gdk', '3.0')
+    gi.require_version("Gtk", "3.0")
+    from gi.repository import Gtk, Gdk
+    # create a new window
+    window = Gtk.Window()
+    window.set_type_hint(Gdk.WindowType.TOPLEVEL)
+    window.set_size_request(200, 100)
+    window.set_resizable(False)
+    window.set_title(_("Type Text"))
+    window.connect("delete_event", lambda w, e: Gtk.main_quit())
+
+    vbox = Gtk.VBox()
+    vbox.set_homogeneous(False)
+    vbox.set_spacing(0)
+    window.add(vbox)
+    vbox.show()
+
+    entry = Gtk.Entry()
+    entry.set_max_length(50)
+    vbox.pack_start(entry, True, True, 0)
+    entry.show()
+
+    hbox = Gtk.HBox()
+    hbox.set_homogeneous(False)
+    hbox.set_spacing(0)
+    vbox.add(hbox)
+    hbox.show()
+
+    button = Gtk.Button(stock=Gtk.STOCK_CLOSE)
+    button.connect("clicked", lambda w: Gtk.main_quit())
+    vbox.pack_start(button, False, False, 0)
+    button.set_can_default(True)
+    button.grab_default()
+    button.show()
+    window.show()
+
+    Gtk.main()
+
+
+def main(args):
+
+    gettext.textdomain("checkbox")
+
+    if "DISPLAY" in os.environ:
+        gtk_prompt()
+    else:
+        cli_prompt()
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/org.oniroproject.integration-tests/bin/memory_compare.py b/org.oniroproject.integration-tests/bin/memory_compare.py
new file mode 100755
index 0000000000000000000000000000000000000000..aaf89f481e9f242d9801feb1e5339ed41c2b518e
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/memory_compare.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This file is part of Checkbox.
+#
+# Copyright 2014 Canonical Ltd.
+#
+# Authors:
+#    Brendan Donegan <brendan.donegan@canonical.com>
+#    Jeff Lane <jeff.lane@canonical.com>
+#    Sylvain Pineau <sylvain.pineau@canonical.com>
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import re
+from subprocess import check_output, CalledProcessError, PIPE
+
+from checkbox_support.helpers.human_readable_bytes import HumanReadableBytes
+from checkbox_support.parsers.lshwjson import LshwJsonParser
+from checkbox_support.parsers.meminfo import MeminfoParser
+
+
+class LshwJsonResult:
+
+    memory_reported = 0
+    banks_reported = 0
+
+    # jlane LP:1525009
+    # Discovered the case can change, my x86 systems used "System Memory"
+    # Failing ARM system used "System memory"
+    desc_regex = re.compile('System Memory', re.IGNORECASE)
+
+    def addHardware(self, hardware):
+        if self.desc_regex.match(str(hardware.get('description', 0))):
+            self.memory_reported += int(hardware.get('size', 0))
+        elif 'bank' in hardware['id']:
+            self.banks_reported += int(hardware.get('size', 0))
+
+
+def get_installed_memory_size():
+    try:
+        output = check_output(['lshw', '-json'],
+                              universal_newlines=True,
+                              stderr=PIPE)
+    except CalledProcessError:
+        return 0
+    lshw = LshwJsonParser(output)
+    result = LshwJsonResult()
+    lshw.run(result)
+
+    if result.memory_reported:
+        return result.memory_reported
+    else:
+        return result.banks_reported
+
+
+class MeminfoResult:
+
+    memtotal = 0
+
+    def setMemory(self, memory):
+        self.memtotal = memory['total']
+
+
+def get_visible_memory_size():
+    parser = MeminfoParser(open('/proc/meminfo'))
+    result = MeminfoResult()
+    parser.run(result)
+
+    return result.memtotal
+
+
+def get_threshold(installed_memory):
+    GB = 1024**3
+    if installed_memory <= 2 * GB:
+        return 25
+    elif installed_memory <= 6 * GB:
+        return 20
+    else:
+        return 10
+
+
+def main():
+    if os.geteuid() != 0:
+        print("This script must be run as root.", file=sys.stderr)
+        return 1
+
+    installed_memory = HumanReadableBytes(get_installed_memory_size())
+    visible_memory = HumanReadableBytes(get_visible_memory_size())
+    threshold = get_threshold(installed_memory)
+
+    difference = HumanReadableBytes(installed_memory - visible_memory)
+    try:
+        percentage = difference / installed_memory * 100
+    except ZeroDivisionError:
+        print("Results:")
+        print("\t/proc/meminfo reports:\t{}".format(visible_memory),
+              file=sys.stderr)
+        print("\tlshw reports:\t{}".format(installed_memory),
+              file=sys.stderr)
+        print("\nFAIL: Either lshw or /proc/meminfo returned a memory size "
+              "of 0 kB", file=sys.stderr)
+        return 1
+
+    if percentage <= threshold:
+        print("Results:")
+        print("\t/proc/meminfo reports:\t{}".format(visible_memory))
+        print("\tlshw reports:\t{}".format(installed_memory))
+        print("\nPASS: Meminfo reports %s less than lshw, a "
+              "difference of %.2f%%. This is less than the "
+              "%d%% variance allowed." % (difference, percentage, threshold))
+        return 0
+    else:
+        print("Results:", file=sys.stderr)
+        print("\t/proc/meminfo reports:\t{}".format(visible_memory),
+              file=sys.stderr)
+        print("\tlshw reports:\t{}".format(installed_memory), file=sys.stderr)
+        print("\nFAIL: Meminfo reports %d less than lshw, "
+              "a difference of %.2f%%. Only a variance of %d%% in "
+              "reported memory is allowed." %
+              (difference, percentage, threshold), file=sys.stderr)
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/net_driver_info.py b/org.oniroproject.integration-tests/bin/net_driver_info.py
new file mode 100755
index 0000000000000000000000000000000000000000..3c29516d730dadea3a115ba422791d68bb8d1955
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/net_driver_info.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# Copyright 2017-2018 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#   Jonathan Cave <jonathan.cave@canonical.com>
+#   Taihsiang Ho <taihsiang.ho@canonical.com>
+#
+# Print info about drivers we can identify automatically and also those we
+# identify in the special interest list!
+
+from pathlib import Path
+import subprocess as sp
+import sys
+
+# Store pairs of (interface, driver)
+driver_list = []
+
+# Find drivers in sysfs
+for interface in Path("/sys/class/net").iterdir():
+    mod_path = Path("{}/device/driver/module".format(interface))
+    if mod_path.is_symlink():
+        driver_list.append((interface.name, mod_path.resolve().name))
+
+# Add user requested modules to the list. Create "unknown" interfaces if none
+# of the identified interfaces above are using it
+for user_driver in sys.argv[1:]:
+    if user_driver:
+        if Path("/sys/module/{}".format(user_driver)).exists():
+            if not any(x[1] == user_driver for x in driver_list):
+                driver_list.append(("unknown", user_driver))
+        else:
+            print("Requested driver {} not loaded\n".format(user_driver))
+
+# Produce the output
+for interface, driver in driver_list:
+    print("Interface {} using module {}".format(interface, driver))
+    sysfs_path = Path("/sys/module/{}/parameters".format(driver))
+    if sysfs_path.is_dir():
+        print("  Parameters:")
+        for path in Path(sysfs_path).iterdir():
+            if path.is_file():
+                # Path.read_text is new in python 3.5 but we want to support
+                # trusty as well, which uses python 3.4 by default.
+                with open(str(path), 'r') as f:
+                    print("    {}: {}".format(path.name, f.read().strip()))
+    print()
+    print('Checking kernel ring buffer for {} messages:'.format(driver))
+    cmd = "dmesg -T -x | grep {} || true".format(driver)
+    output = sp.check_output(cmd, shell=True)
+    print(output.decode(sys.stdout.encoding))
+    print()
diff --git a/org.oniroproject.integration-tests/bin/network_device_info.py b/org.oniroproject.integration-tests/bin/network_device_info.py
new file mode 100755
index 0000000000000000000000000000000000000000..9d31f75a886eb128f4e3b33c9d304921a8fe9633
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/network_device_info.py
@@ -0,0 +1,439 @@
+#!/usr/bin/env python3
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Parts of this are based on the example python code that ships with
+# NetworkManager
+# http://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python
+#
+# Copyright (C) 2012-2019 Canonical, Ltd.
+
+import argparse
+import fcntl
+from natsort import natsorted
+import os
+import socket
+import struct
+from subprocess import check_output, CalledProcessError, STDOUT
+import sys
+
+import dbus
+
+from checkbox_support.parsers.modinfo import ModinfoParser
+from checkbox_support.parsers.udevadm import UdevadmParser
+
+
+class Utils():
+
+    sys_path = '/sys/class/net'
+
+    @staticmethod
+    def get_ethtool_info(interface, key):
+        """Return ethtool information identified by key"""
+        cmd = ['/sbin/ethtool', interface]
+        try:
+            output = check_output(cmd, stderr=STDOUT, universal_newlines=True)
+        except CalledProcessError:
+            return "ethtool returned error"
+        except FileNotFoundError:
+            return "ethtool not installed"
+        if not output:
+            return "ethtool returned no output"
+        data = []
+        output_line = False
+        for line in iter(output.splitlines()):
+            if key in line:
+                label_start = line.find(key)
+                data_start = label_start + len(key)
+                output_line = True
+            if (output_line is True and line[label_start] != ' ' and
+                    key not in line):
+                output_line = False
+            if output_line:
+                data.append(line[data_start:].strip())
+                if len(data) == 0:
+                    data.append("Not reported")
+            result = ("\n" + "".join(["\t" + item + "\n"
+                                      for item in natsorted(data)]))
+        result = result.rstrip()
+        return result
+
+    @classmethod
+    def is_iface_connected(cls, iface):
+        try:
+            carrier_file = os.path.join(cls.sys_path, iface, 'carrier')
+            return int(open(carrier_file, 'r').read()) == 1
+        except Exception:
+            pass
+        return False
+
+    @staticmethod
+    def get_ipv4_address(interface):
+        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        try:
+            ipv4_addr = socket.inet_ntoa(fcntl.ioctl(
+                s.fileno(),
+                0x8915,  # SIOCGIFADDR
+                struct.pack('256s', interface[:15].encode())
+            )[20:24])
+        except Exception as e:
+            print("ERROR: getting the IPv4 address for %s: %s" %
+                  (interface, repr(e)))
+            ipv4_addr = "***NOT CONFIGURED***"
+        finally:
+            return ipv4_addr
+
+    @staticmethod
+    def get_ipv6_address(interface):
+        cmd = ['/sbin/ip', '-6', '-o', 'addr', 'show', 'dev', interface,
+               'scope', 'link']
+        proc = check_output(cmd, universal_newlines=True)
+        return proc.split()[3].strip()
+
+    @classmethod
+    def get_mac_address(cls, interface):
+        address_file = os.path.join(cls.sys_path, interface, 'address')
+        try:
+            return open(address_file, 'r').read().strip()
+        except IOError:
+            return 'UNKNOWN'
+
+    @classmethod
+    def get_speed(cls, interface):
+        speed_file = os.path.join(cls.sys_path, interface, 'speed')
+        try:
+            return open(speed_file, 'r').read().strip()
+        except IOError:
+            return 'UNKNOWN'
+
+    @staticmethod
+    def get_driver_version(driver):
+        cmd = ['/sbin/modinfo', driver]
+        try:
+            output = check_output(cmd, stderr=STDOUT, universal_newlines=True)
+        except CalledProcessError:
+            return None
+        if not output:
+            return None
+        parser = ModinfoParser(output)
+        modinfo = parser.get_all()
+
+        # try the version field first, then vermagic second, some audio
+        # drivers don't report version if the driver is in-tree
+        version = modinfo.get('version')
+
+        if version is None or version == 'in-tree':
+            # vermagic will look like this (below) and we only care about the
+            # first part:
+            # "3.2.0-29-generic SMP mod_unload modversions"
+            version = modinfo.get('vermagic').split()[0]
+
+        return version
+
+
+class NetworkDeviceInfo():
+
+    def __init__(self):
+        self._category = None
+        self._interface = None
+        self._product = None
+        self._vendor = None
+        self._driver = None
+        self._driver_version = None
+        self._firmware_missing = None
+        self._path = None
+        self._id = None
+        self._subsystem_id = None
+        self._mac = None
+        self._carrier_status = None
+        self._ipv4 = None
+        self._ipv6 = None
+        self._speed = None
+        self._supported_modes = None
+        self._advertised_modes = None
+        self._partner_modes = None
+
+    def __str__(self):
+        ret = ""
+        for key, val in vars(self).items():
+            if val is not None:
+                # leading _ removed, remaining ones spaces
+                pretty_key = key.lstrip('_').replace('_', ' ').title()
+                ret += '{}: {}\n'.format(pretty_key, val)
+        return ret
+
+    @property
+    def interface(self):
+        return self._interface
+
+    @interface.setter
+    def interface(self, value):
+        self._interface = value
+        self._interface_populate()
+
+    @property
+    def carrier_status(self):
+        return self._carrier_status
+
+    @property
+    def driver(self):
+        return self._driver
+
+    @driver.setter
+    def driver(self, value):
+        self._driver = value
+        self._driver_populate()
+
+    @property
+    def category(self):
+        return self._category
+
+    @category.setter
+    def category(self, value):
+        self._category = value
+
+    @property
+    def product(self):
+        return self._product
+
+    @product.setter
+    def product(self, value):
+        self._product = value
+
+    @property
+    def vendor(self):
+        return self._vendor
+
+    @vendor.setter
+    def vendor(self, value):
+        self._vendor = value
+
+    @property
+    def firmware_missing(self):
+        return self._firmware_missing
+
+    @firmware_missing.setter
+    def firmware_missing(self, value):
+        self._firmware_missing = value
+
+    @property
+    def path(self):
+        return self._path
+
+    @path.setter
+    def path(self, value):
+        self._path = value
+
+    @property
+    def id(self):
+        return self._id
+
+    @id.setter
+    def id(self, value):
+        self._id = value
+
+    @property
+    def subsystem_id(self):
+        return self._subsystem_id
+
+    @subsystem_id.setter
+    def subsystem_id(self, value):
+        self._subsystem_id = value
+
+    def _interface_populate(self):
+        """Get extra attributes based on interface"""
+        if self.interface is None:
+            return
+        self._mac = Utils.get_mac_address(self.interface)
+        self._supported_modes = (Utils.get_ethtool_info(
+            self.interface, "Supported link modes:"))
+        self._advertised_modes = (Utils.get_ethtool_info(
+            self.interface, "Advertised link modes:"))
+        if Utils.is_iface_connected(self.interface):
+            self._carrier_status = 'Connected'
+            self._ipv4 = Utils.get_ipv4_address(self.interface)
+            self._ipv6 = Utils.get_ipv6_address(self.interface)
+            self._speed = Utils.get_speed(self.interface)
+            self._partner_modes = (Utils.get_ethtool_info(
+                self.interface, "Link partner advertised link modes:"))
+        else:
+            self._carrier_status = 'Disconnected'
+
+    def _driver_populate(self):
+        """Get extra attributes based on driver"""
+        if self.driver is None:
+            return
+        self._driver_version = Utils.get_driver_version(self.driver)
+
+
+class NMDevices():
+
+    # This example lists basic information about network interfaces known to NM
+    devtypes = {1: "Ethernet",
+                2: "WiFi",
+                5: "Bluetooth",
+                6: "OLPC",
+                7: "WiMAX",
+                8: "Modem"}
+
+    def __init__(self, category="NETWORK"):
+        self.category = category
+        self._devices = []
+        self._collect_devices()
+
+    def __len__(self):
+        return len(self._devices)
+
+    def _collect_devices(self):
+        bus = dbus.SystemBus()
+        proxy = bus.get_object("org.freedesktop.NetworkManager",
+                               "/org/freedesktop/NetworkManager")
+        manager = dbus.Interface(proxy, "org.freedesktop.NetworkManager")
+        self._devices = manager.GetDevices()
+
+    def devices(self):
+        """Convert to list of NetworkDevice with NM derived attrs set"""
+        for d in self._devices:
+            bus = dbus.SystemBus()
+            dev_proxy = bus.get_object("org.freedesktop.NetworkManager", d)
+            prop_iface = dbus.Interface(dev_proxy,
+                                        "org.freedesktop.DBus.Properties")
+            props = prop_iface.GetAll("org.freedesktop.NetworkManager.Device")
+            devtype = self.devtypes.get(props.get('DeviceType'))
+            if devtype is None:
+                continue
+            nd = NetworkDeviceInfo()
+            if self.category == "NETWORK":
+                if devtype == "Ethernet":
+                    nd.category = self.category
+                else:
+                    continue
+            if self.category == "WIRELESS":
+                if devtype == "WiFi":
+                    nd.category = self.category
+                else:
+                    continue
+            nd.interface = props.get('Interface')
+            nd.driver = props.get('Driver')
+            nd.firmware_missing = props.get('FirmwareMissing')
+            yield nd
+
+
+class UdevDevices():
+
+    def __init__(self, category='NETWORK'):
+        self.category = category
+        self._devices = []
+        self._collect_devices()
+
+    def __len__(self):
+        return len(self._devices)
+
+    def _collect_devices(self):
+        cmd = ['udevadm', 'info', '--export-db']
+        try:
+            output = check_output(cmd).decode(sys.stdout.encoding,
+                                              errors='ignore')
+        except CalledProcessError as err:
+            sys.stderr.write(err)
+            return
+        udev = UdevadmParser(output)
+        for device in udev.run():
+            if (device.category == self.category and
+                    device.interface != 'UNKNOWN'):
+                self._devices.append(device)
+
+    def devices(self):
+        """Convert to list of NetworkDevice with UDev derived attrs set"""
+        for device in self._devices:
+            nd = NetworkDeviceInfo()
+            nd.category = getattr(device, 'category', None)
+            nd.interface = getattr(device, 'interface', None)
+            nd.product = getattr(device, 'product', None)
+            nd.vendor = getattr(device, 'vendor', None)
+            nd.driver = getattr(device, 'driver', None)
+            nd.path = getattr(device, 'path', None)
+            vid = getattr(device, 'vendor_id', None)
+            pid = getattr(device, 'product_id', None)
+            if vid and pid:
+                nd.id = '[{0:04x}:{1:04x}]'.format(vid, pid)
+            svid = getattr(device, 'subvendor_id', None)
+            spid = getattr(device, 'subproduct_id', None)
+            if svid and spid:
+                nd.subsystem_id = '[{0:04x}:{1:04x}]'.format(svid, spid)
+            yield nd
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        description='Gather information about network devices')
+    parser.add_argument('action', choices=('detect', 'info'),
+                        help='Detect mode or just report info')
+    parser.add_argument('category', choices=('NETWORK', 'WIRELESS'),
+                        help='Either ethernet or WLAN devices')
+    parser.add_argument('--no-nm', action='store_true',
+                        help='Don\'t attempt to get info from network manager')
+    parser.add_argument('--interface',
+                        help='Restrict info action to specified interface')
+    parser.add_argument('--fail-on-disconnected', action='store_true',
+                        help=('Script will exit with a non-zero return code if'
+                              ' any interface is not connected'))
+    args = parser.parse_args()
+
+    udev = UdevDevices(args.category)
+    disconnected_ifaces = []
+
+    # The detect action should indicate presence of a device belonging to the
+    # category and cause the job to fail if none present
+    if args.action == 'detect':
+        if len(udev) == 0:
+            raise SystemExit('No devices detected by udev')
+        else:
+            print("[ Devices found by udev ]".center(80, '-'))
+            for device in udev.devices():
+                print(device)
+
+    # The info action should just gather infomation about any ethernet devices
+    # found and report for inclusion in e.g. an attachment job and include
+    # NetworkManager as a source if desired
+    if args.action == 'info':
+        # If interface has been specified
+        if args.interface:
+            for device in udev.devices():
+                if device.interface == args.interface:
+                    print(device)
+            sys.exit(0)
+
+        # Report udev detected devices first
+        print("[ Devices found by udev ]".center(80, '-'))
+        for device in udev.devices():
+            print(device)
+            if device.carrier_status == "Disconnected":
+                disconnected_ifaces.append(device.interface)
+
+        # Attempt to report devices found by NetworkManager. This can be
+        # skipped as doesn't make sense for server installs
+        if not args.no_nm:
+            nm = NMDevices(args.category)
+            print("[ Devices found by Network Manager ]".center(80, '-'))
+            for device in nm.devices():
+                print(device)
+
+        if disconnected_ifaces and args.fail_on_disconnected:
+            print("WARNING: The following interfaces are not connected:")
+            for iface in disconnected_ifaces:
+                print(iface)
+            sys.exit(1)
+
+    sys.exit(0)
diff --git a/org.oniroproject.integration-tests/bin/pm_log_check.py b/org.oniroproject.integration-tests/bin/pm_log_check.py
new file mode 100755
index 0000000000000000000000000000000000000000..1d41ed68723917126551e197e3938a56d040eb05
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/pm_log_check.py
@@ -0,0 +1,271 @@
+#!/usr/bin/env python3
+import os
+import sys
+import re
+import difflib
+import logging
+from argparse import ArgumentParser
+
+# Script return codes
+SUCCESS = 0
+NOT_MATCH = 1
+NOT_FINISHED = 2
+NOT_FOUND = 3
+
+
+def main():
+    args = parse_args()
+
+    if not os.path.isfile(args.input_log_filename):
+        sys.stderr.write('Log file {0!r} not found\n'
+                         .format(args.input_log_filename))
+        sys.exit(NOT_FOUND)
+
+    LoggingConfiguration.set(args.log_level,
+                             args.output_log_filename)
+    parser = Parser(args.input_log_filename)
+    results = parser.parse()
+
+    if not compare_results(results):
+        sys.exit(NOT_MATCH)
+
+    sys.exit(SUCCESS)
+
+
+class Parser(object):
+    """
+    Reboot test log file parser
+    """
+    is_logging_line = (re.compile(
+        r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}').search)
+    is_getting_info_line = (re.compile('Gathering hardware information...$')
+                            .search)
+    is_executing_line = (re.compile("Executing: '(?P<command>.*)'...$")
+                         .search)
+    is_output_line = re.compile('Output:$').search
+    is_field_line = (re.compile('^- (?P<field>returncode|stdout|stderr):$')
+                     .match)
+    is_test_complete_line = re.compile('test complete$').search
+
+    def __init__(self, filename):
+        self.filename = filename
+
+    def parse(self):
+        """
+        Parse log file and return results
+        """
+        with open(self.filename) as f:
+            results = self._parse_file(LineIterator(f))
+        return results
+
+    def _parse_file(self, iterator):
+        """
+        Parse all lines in iterator and return results
+        """
+        results = []
+        result = {}
+
+        for line in iterator:
+            if self.is_getting_info_line(line):
+                if result:
+                    # Add last result to list of results
+                    results.append(result)
+
+                # Initialize for a new iteration results
+                result = {}
+
+            match = self.is_executing_line(line)
+            if match:
+                command = match.group('command')
+                command_output = self._parse_command_output(iterator)
+
+                if command_output is not None:
+                    result[command] = command_output
+        else:
+            if result:
+                # Add last result to list of results
+                results.append(result)
+
+        if not self.is_test_complete_line(line):
+            sys.stderr.write("Test didn't finish properly according to logs\n")
+            sys.exit(NOT_FINISHED)
+
+        return results
+
+    def _parse_command_output(self, iterator):
+        """
+        Parse one command output
+        """
+        command_output = None
+
+        # Skip all lines until command output is found
+        for line in iterator:
+            if self.is_output_line(line):
+                command_output = {}
+                break
+            if (
+                self.is_executing_line(line) or
+                self.is_getting_info_line(line)
+            ):
+                # Skip commands with no output
+                iterator.unnext(line)
+                return None
+
+        # Parse command output message
+        for line in iterator:
+            match = self.is_field_line(line)
+            if match:
+                field = match.group('field')
+                value = self._parse_command_output_field(iterator)
+                command_output[field] = value
+            # Exit when all command output fields
+            # have been gathered
+            else:
+                iterator.unnext(line)
+                break
+
+        return command_output
+
+    def _parse_command_output_field(self, iterator):
+        """
+        Parse one command output field
+        """
+        # Accummulate as many lines as needed
+        # for the field value
+        value = []
+        for line in iterator:
+            if (
+                self.is_logging_line(line) or
+                self.is_field_line(line)
+            ):
+                iterator.unnext(line)
+                break
+
+            value.append(line)
+
+        value = ''.join(value)
+        return value
+
+
+class LineIterator:
+    """
+    Iterator wrapper to make it possible
+    to push back lines that shouldn't have been consumed
+    """
+
+    def __init__(self, iterator):
+        self.iterator = iterator
+        self.buffer = []
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self.buffer:
+            return self.buffer.pop()
+
+        return next(self.iterator)
+
+    def unnext(self, line):
+        self.buffer.append(line)
+
+
+class LoggingConfiguration(object):
+    @classmethod
+    def set(cls, log_level, log_filename):
+        """
+        Configure a rotating file logger
+        """
+        logger = logging.getLogger()
+        logger.setLevel(logging.DEBUG)
+
+        # Log to sys.stderr using log level passed through command line
+        if log_level != logging.NOTSET:
+            log_handler = logging.StreamHandler()
+            formatter = logging.Formatter('%(levelname)-8s %(message)s')
+            log_handler.setFormatter(formatter)
+            log_handler.setLevel(log_level)
+            logger.addHandler(log_handler)
+
+        # Log to rotating file using DEBUG log level
+        log_handler = logging.FileHandler(log_filename, mode='w')
+        formatter = logging.Formatter('%(asctime)s %(levelname)-8s '
+                                      '%(message)s')
+        log_handler.setFormatter(formatter)
+        log_handler.setLevel(logging.DEBUG)
+        logger.addHandler(log_handler)
+
+
+def compare_results(results):
+    """
+    Compare results using first one as a baseline
+    """
+    baseline = results[0]
+
+    success = True
+    for index, result in enumerate(results[1:]):
+        for command in baseline.keys():
+            baseline_output = baseline[command]
+            result_output = result[command]
+
+            error_messages = []
+            fields = (set(baseline_output.keys()) |
+                      set(result_output.keys()))
+            for field in fields:
+                baseline_field = baseline_output.get(field, '')
+                result_field = result_output.get(field, '')
+
+                if baseline_field != result_field:
+                    differ = difflib.Differ()
+
+                    message = ["** {field!r} field doesn't match:"
+                               .format(field=field)]
+                    comparison = differ.compare(baseline_field.splitlines(),
+                                                result_field.splitlines())
+                    message.extend(list(comparison))
+                    error_messages.append('\n'.join(message))
+
+            if not error_messages:
+                logging.debug('[Iteration {0}] {1}...\t[OK]'
+                              .format(index + 1, command))
+            else:
+                success = False
+                if command.startswith('fwts'):
+                    logging.error('[Iteration {0}] {1}...\t[FAIL]'
+                                  .format(index + 1, command))
+                else:
+                    logging.error('[Iteration {0}] {1}...\t[FAIL]\n'
+                                  .format(index + 1, command))
+                    for message in error_messages:
+                        logging.error(message)
+
+    return success
+
+
+def parse_args():
+    """
+    Parse command-line arguments
+    """
+    parser = ArgumentParser(description=('Check power management '
+                                         'test case results'))
+    parser.add_argument('input_log_filename', metavar='log_filename',
+                        help=('Path to the input log file '
+                              'on which to perform the check'))
+    parser.add_argument('output_log_filename', metavar='log_filename',
+                        help=('Path to the output log file '
+                              'for the results of the check'))
+    log_levels = ['notset', 'debug', 'info', 'warning', 'error', 'critical']
+    parser.add_argument('--log-level', dest='log_level', default='info',
+                        choices=log_levels,
+                        help=('Log level. '
+                              'One of {0} or {1} (%(default)s by default)'
+                              .format(', '.join(log_levels[:-1]),
+                                      log_levels[-1])))
+    args = parser.parse_args()
+    args.log_level = getattr(logging, args.log_level.upper())
+
+    return args
+
+
+if __name__ == '__main__':
+    main()
diff --git a/org.oniroproject.integration-tests/bin/pm_test.py b/org.oniroproject.integration-tests/bin/pm_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..92a5863cce32b1e0b684ba1bbf255c1a849fb70a
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/pm_test.py
@@ -0,0 +1,1030 @@
+#!/usr/bin/env python3
+"""
+If you're debugging this program, set PM_TEST_DRY_RUN in your environment.
+It will make the script not run actual S3, S4, reboot and poweroff commands.
+"""
+import gi
+import json
+import logging
+import logging.handlers
+import os
+import pwd
+import re
+import shutil
+import signal
+import subprocess
+import sys
+from argparse import ArgumentParser, SUPPRESS
+from calendar import timegm
+from configparser import ConfigParser
+from datetime import datetime, timedelta
+from time import localtime, time
+gi.require_version("Gtk", "3.0")
+from gi.repository import GObject, Gtk  # noqa: E402
+
+
+def main():
+    """
+    Run power management operation as many times as needed
+    """
+    args, extra_args = MyArgumentParser().parse()
+
+    # Verify that script is run as root
+    if os.getuid():
+        sys.stderr.write('This script needs superuser '
+                         'permissions to run correctly\n')
+        sys.exit(1)
+
+    # Obtain name of the invoking user.
+    username = os.getenv('NORMAL_USER')
+    if not username:
+        uid = os.getenv('SUDO_UID') or os.getenv('PKEXEC_UID')
+        if not uid:
+            sys.stderr.write('Unable to determine invoking user\n')
+            sys.exit(1)
+        username = pwd.getpwuid(int(uid)).pw_name
+
+    LoggingConfiguration.set(args.log_level, args.log_filename, args.append)
+    logging.debug('Invoking username: %s', username)
+    logging.debug('Arguments: {0!r}'.format(args))
+    logging.debug('Extra Arguments: {0!r}'.format(extra_args))
+
+    dry_run = os.environ.get('PM_TEST_DRY_RUN', False)
+    if dry_run:
+        logging.info("Running in dry-run mode")
+
+    try:
+        operation = PowerManagementOperation(
+            args, extra_args, user=username, dry_run=dry_run)
+        operation.setup()
+        operation.run()
+    except (TestCancelled, TestFailed) as exception:
+        if isinstance(exception, TestFailed):
+            logging.error(exception.args[0])
+        message = exception.MESSAGE.format(args.pm_operation.capitalize())
+        if args.silent:
+            logging.info(message)
+        else:
+            title = '{0} test'.format(args.pm_operation.capitalize())
+            MessageDialog(title, message, Gtk.MessageType.ERROR).run()
+        operation.teardown()
+        result = {
+            'outcome': 'fail',
+            'comments': message,
+        }
+        with open(os.path.join(args.log_dir, '__result'), 'wt') as f:
+            json.dump(result, f)
+        env = os.environ.copy()
+        # remove following envvars
+        for key in ['LD_LIBRARY_PATH', 'PYTHONPATH', 'PYTHONHOME']:
+            if key in env.keys():
+                del env[key]
+        env['DISPLAY'] = ':0'
+        sudo_cmd = 'sudo -u {} bash -c "source {}; exec bash"'.format(
+            operation.user, args.checkbox_respawn_cmd)
+        args = ['x-terminal-emulator', '-e', sudo_cmd]
+        print("\nCheckbox will resume in another window.")
+        print("It's safe to close this one.", flush=True)
+        os.execvpe('x-terminal-emulator', args, env)
+
+    return 0
+
+
+class PowerManagementOperation():
+    SLEEP_TIME = 5
+
+    def __init__(self, args, extra_args, user=None, dry_run=False):
+        self.args = args
+        self.extra_args = extra_args
+        self.user = user
+        self.dry_run = dry_run
+        self.hw_list_start = os.path.join(
+            self.args.log_dir, 'hardware.at_start')
+
+    def setup(self):
+        """
+        Enable configuration file
+        """
+        if self.args.check_hardware_list:
+            if not os.path.exists(self.hw_list_start):
+                # create baseline list once per session
+                with open(self.hw_list_start, 'wt') as f:
+                    f.write(self.get_hw_list())
+
+        # Enable autologin and sudo on first cycle
+        if self.args.total == self.args.repetitions:
+            AutoLoginConfigurator(user=self.user).enable()
+            SudoersConfigurator(user=self.user).enable()
+
+        # Schedule this script to be automatically executed
+        # on startup to continue testing
+        autostart_file = AutoStartFile(self.args, user=self.user)
+        autostart_file.write()
+
+    def run(self):
+        """
+        Run a power management iteration
+        """
+        logging.info('{0} operations remaining: {1}'
+                     .format(self.args.pm_operation, self.args.repetitions))
+        if self.args.pm_timestamp:
+            pm_timestamp = datetime.fromtimestamp(self.args.pm_timestamp)
+            now = datetime.now()
+            pm_time = now - pm_timestamp
+            logging.info('{0} time: {1}'
+                         .format(self.args.pm_operation.capitalize(), pm_time))
+        if self.args.repetitions > 0:
+            self.run_suspend_cycles(self.args.suspends_before_reboot,
+                                    self.args.fwts)
+            self.run_pm_command()
+            if self.args.check_hardware_list:
+                self.check_hw_list()
+        else:
+            self.summary()
+
+    def run_pm_command(self):
+        """
+        Run power managment command and check result if needed
+        """
+        # Display information to user
+        # and make it possible to cancel the test
+        CountdownDialog(self.args.pm_operation,
+                        self.args.pm_delay,
+                        self.args.hardware_delay,
+                        self.args.total - self.args.repetitions,
+                        self.args.total).run()
+
+        # A small sleep time is added to reboot and poweroff
+        # so that script has time to return a value
+        # (useful when running it as an automated test)
+        command_str = ('sleep {0}; {1}'
+                       .format(self.SLEEP_TIME, self.args.pm_operation))
+        if self.extra_args:
+            command_str += ' {0}'.format(' '.join(self.extra_args))
+
+        if self.args.pm_operation != 'reboot':
+            WakeUpAlarm.set(seconds=self.args.wakeup)
+
+        logging.info('Executing new {0!r} operation...'
+                     .format(self.args.pm_operation))
+        logging.debug('Executing: {0!r}...'.format(command_str))
+        if self.dry_run:
+            print("\n\nRUNNING IN DRY-RUN MODE")
+            print("Normally the program would run: {}".format(command_str))
+            print("Waiting for Enter instead")
+            input()
+        else:
+            # The PM operation is performed asynchronously so let's just wait
+            # indefinitely until it happens and someone comes along to kill us.
+            # This addresses LP: #1413134
+            subprocess.check_call(command_str, shell=True)
+            signal.pause()
+
+    def run_suspend_cycles(self, cycles_count, fwts):
+        """Run suspend and resume cycles."""
+        if cycles_count < 1:
+            return
+        script_path = ''
+        if fwts:
+            script_path = 'checkbox-support-fwts_test'
+            command_tpl = '-s s3 --s3-device-check ' \
+                          '--s3-sleep-delay=30 --s3-multiple={}'
+            if self.args.log_dir:
+                command_tpl = '--log={}/fwts.log '.format(
+                    self.args.log_dir) + command_tpl
+            command_tpl = '{} ' + command_tpl
+        else:
+            script_name = 'sleep_test.py'
+            command_tpl = '{} -s mem -p -i {} -w 10'
+            script_path = os.path.join(
+                os.path.dirname(os.path.realpath(__file__)), script_name)
+        command_str = command_tpl.format(script_path, cycles_count)
+        logging.info('Running suspend/resume cycles')
+        logging.debug('Executing: {0!r}...'.format(command_str))
+        if self.dry_run:
+            print("\n\nRUNNING IN DRY-RUN MODE")
+            print("Normally the program would run: {}".format(command_str))
+            print("Waiting for Enter instead")
+            input()
+        else:
+            try:
+                # We call sleep_test.py or fwts_test script and log its output
+                # as it contains average times we need to compute global
+                # average times later.
+                logging.info(subprocess.check_output(
+                    command_str, universal_newlines=True, shell=True))
+            except subprocess.CalledProcessError as e:
+                logging.error('Error while running {0}:'.format(e.cmd))
+                logging.error(e.output)
+
+    def summary(self):
+        """
+        Gather hardware information for the last time,
+        log execution time and exit
+        """
+        # Just gather hardware information one more time and exit
+        CountdownDialog(self.args.pm_operation,
+                        self.args.pm_delay,
+                        self.args.hardware_delay,
+                        self.args.total - self.args.repetitions,
+                        self.args.total).run()
+
+        self.teardown()
+
+        # Log some time information
+        start = datetime.fromtimestamp(self.args.start)
+        end = datetime.now()
+        if self.args.pm_operation == 'reboot':
+            sleep_time = timedelta(seconds=self.SLEEP_TIME)
+        else:
+            sleep_time = timedelta(seconds=self.args.wakeup)
+
+        wait_time = timedelta(seconds=(self.args.pm_delay +
+                                       self.args.hardware_delay *
+                                       self.args.total))
+        average = (end - start - wait_time) / self.args.total - sleep_time
+        time_message = ('Total elapsed time: {total}\n'
+                        'Average recovery time: {average}'
+                        .format(total=end - start,
+                                average=average))
+        logging.info(time_message)
+
+        message = ('{0} test complete'
+                   .format(self.args.pm_operation.capitalize()))
+        if self.args.suspends_before_reboot:
+            total_suspends_expected = (
+                self.args.suspends_before_reboot * self.args.total)
+            problems = ''
+            fwts_log_path = os.path.join(self.args.log_dir, 'fwts.log')
+            try:
+                with open(fwts_log_path, 'rt') as f:
+                    magic_line_s3 = 'Completed S3 cycle(s) \n'
+                    magic_line_s2idle = 'Completed s2idle cycle(s) \n'
+                    lines = f.readlines()
+                    count_s3 = lines.count(magic_line_s3)
+                    count_s2idle = lines.count(magic_line_s2idle)
+                    count = count_s3 + count_s2idle
+                    if count != total_suspends_expected:
+                        problems = (
+                            "Found {} occurrences of S3/S2idle."
+                            " Expected {}".format(
+                                count, total_suspends_expected))
+            except FileNotFoundError:
+                problems = "Error opening {}".format(fwts_log_path)
+            if problems:
+                result = {
+                    'outcome': 'fail' if problems else 'pass',
+                    'comments': problems,
+                }
+                result_filename = os.path.join(self.args.log_dir, '__result')
+                with open(result_filename, 'wt') as f:
+                    json.dump(result, f)
+
+        if self.args.silent:
+            logging.info(message)
+        else:
+            title = '{0} test'.format(self.args.pm_operation.capitalize())
+            MessageDialog(title, message).run()
+        if self.args.checkbox_respawn_cmd:
+            try:
+                subprocess.run(
+                    r'unset LD_LIBRARY_PATH;'
+                    r'unset PYTHONPATH; unset PYTHONHOME;'
+                    r'DISPLAY=:0 x-terminal-emulator -e "sudo -u '
+                    r'{} bash -c \"source {}; exec bash\""'.format(
+                        self.user, self.args.checkbox_respawn_cmd),
+                    shell=True,
+                    check=True)
+            # x-terminal-emulator command does not work on Wayland
+            # Run the checkbox_respawn_cmd via subprocess.run instead
+            except subprocess.CalledProcessError:
+                with open(self.args.checkbox_respawn_cmd, 'rt') as f:
+                    for l in f:
+                        subprocess.run(l, shell=True)
+
+    def teardown(self):
+        """
+        Restore configuration
+        """
+        # Don't execute this script again on next reboot
+        autostart_file = AutoStartFile(self.args, user=self.user)
+        autostart_file.remove()
+
+        # Restore previous configuration
+        SudoersConfigurator().disable()
+        AutoLoginConfigurator().disable()
+
+    def get_hw_list(self):
+        try:
+            content = subprocess.check_output(
+                'lspci', encoding=sys.stdout.encoding)
+            content += subprocess.check_output(
+                'lsusb', encoding=sys.stdout.encoding)
+            return content
+        except subprocess.CalledProcessError as exc:
+            logging.warning("Problem running lspci or lsusb: %s", exc)
+            return ''
+
+    def check_hw_list(self):
+        with open(self.hw_list_start, 'rt') as f:
+            before = set(f.read().split('\n'))
+        after = set(self.get_hw_list().split('\n'))
+        if after != before:
+            message = "Hardware changed!"
+            only_before = before - after
+            if only_before:
+                message += "\nHardware lost after pm operation:"
+                for item in sorted(list(only_before)):
+                    message += '\n\t{}'.format(item)
+            only_after = after - before
+            if only_after:
+                message += "\nNew hardware found after pm operation:"
+                for item in sorted(list(only_after)):
+                    message += '\n\t{}'.format(item)
+            raise TestFailed(message)
+
+
+class TestCancelled(Exception):
+    RETURN_CODE = 1
+    MESSAGE = '{0} test cancelled by user'
+
+
+class TestFailed(Exception):
+    RETURN_CODE = 2
+    MESSAGE = '{0} test failed'
+
+
+class WakeUpAlarm():
+    ALARM_FILENAME = '/sys/class/rtc/rtc0/wakealarm'
+    RTC_FILENAME = '/proc/driver/rtc'
+
+    @classmethod
+    def set(cls, minutes=0, seconds=0):
+        """
+        Calculate wakeup time and write it to BIOS
+        """
+        now = int(time())
+        timeout = minutes * 60 + seconds
+        wakeup_time_utc = now + timeout
+        wakeup_time_local = timegm(localtime()) + timeout
+
+        subprocess.check_call('echo 0 > %s' % cls.ALARM_FILENAME, shell=True)
+        subprocess.check_call('echo %d > %s'
+                              % (wakeup_time_utc, cls.ALARM_FILENAME),
+                              shell=True)
+
+        with open(cls.ALARM_FILENAME) as alarm_file:
+            wakeup_time_stored_str = alarm_file.read()
+
+            if not re.match(r'\d+', wakeup_time_stored_str):
+                subprocess.check_call('echo "+%d" > %s'
+                                      % (timeout, cls.ALARM_FILENAME),
+                                      shell=True)
+                with open(cls.ALARM_FILENAME) as alarm_file2:
+                    wakeup_time_stored_str = alarm_file2.read()
+                if not re.match(r'\d+', wakeup_time_stored_str):
+                    logging.error('Invalid wakeup time format: {0!r}'
+                                  .format(wakeup_time_stored_str))
+                    sys.exit(1)
+
+            wakeup_time_stored = int(wakeup_time_stored_str)
+            try:
+                logging.debug('Wakeup timestamp: {0} ({1})'
+                              .format(wakeup_time_stored,
+                                      datetime.fromtimestamp(
+                                          wakeup_time_stored).strftime('%c')))
+            except ValueError as e:
+                logging.error(e)
+                sys.exit(1)
+
+            if (
+                (abs(wakeup_time_utc - wakeup_time_stored) > 1) and
+                (abs(wakeup_time_local - wakeup_time_stored) > 1)
+            ):
+                logging.error('Wakeup time not stored correctly')
+                sys.exit(1)
+
+        with open(cls.RTC_FILENAME) as rtc_file:
+            separator_regex = re.compile(r'\s+:\s+')
+            rtc_data = dict([separator_regex.split(line.rstrip())
+                             for line in rtc_file])
+            logging.debug('RTC data:\n{0}'
+                          .format('\n'.join(['- {0}: {1}'.format(*pair)
+                                             for pair in rtc_data.items()])))
+
+            # Verify wakeup time has been set properly
+            # by looking into the alarm_IRQ and alrm_date field
+            if rtc_data['alarm_IRQ'] != 'yes':
+                logging.error('alarm_IRQ not set properly: {0}'
+                              .format(rtc_data['alarm_IRQ']))
+                sys.exit(1)
+
+            if '*' in rtc_data['alrm_date']:
+                logging.error('alrm_date not set properly: {0}'
+                              .format(rtc_data['alrm_date']))
+                sys.exit(1)
+
+
+class Command():
+    """
+    Simple subprocess.Popen wrapper to run shell commands
+    and log their output
+    """
+    def __init__(self, command_str, verbose=True):
+        self.command_str = command_str
+        self.verbose = verbose
+
+        self.process = None
+        self.stdout = None
+        self.stderr = None
+        self.time = None
+
+    def run(self):
+        """
+        Execute shell command and return output and status
+        """
+        logging.debug('Executing: {0!r}...'.format(self.command_str))
+
+        self.process = subprocess.Popen(self.command_str,
+                                        shell=True,
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.PIPE)
+        start = datetime.now()
+        result = self.process.communicate()
+        end = datetime.now()
+        self.time = end - start
+
+        if self.verbose:
+            stdout, stderr = result
+            message = ['Output:\n'
+                       '- returncode:\n{0}'.format(self.process.returncode)]
+            if stdout:
+                if type(stdout) is bytes:
+                    stdout = stdout.decode('utf-8', 'ignore')
+                message.append('- stdout:\n{0}'.format(stdout))
+            if stderr:
+                if type(stderr) is bytes:
+                    stderr = stderr.decode('utf-8', 'ignore')
+                message.append('- stderr:\n{0}'.format(stderr))
+            logging.debug('\n'.join(message))
+
+            self.stdout = stdout
+            self.stderr = stderr
+
+        return self
+
+
+class CountdownDialog(Gtk.Dialog):
+    """
+    Dialog that shows the amount of progress in the reboot test
+    and lets the user cancel it if needed
+    """
+    def __init__(self, pm_operation, pm_delay, hardware_delay,
+                 iterations, iterations_count):
+        self.pm_operation = pm_operation
+        title = '{0} test'.format(pm_operation.capitalize())
+
+        buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,)
+        super(CountdownDialog, self).__init__(title=title,
+                                              buttons=buttons)
+        self.set_default_response(Gtk.ResponseType.CANCEL)
+        self.set_resizable(False)
+        self.set_position(Gtk.WindowPosition.CENTER)
+
+        progress_bar = Gtk.ProgressBar()
+        progress_bar.set_fraction(iterations / float(iterations_count))
+        progress_bar.set_text('{0}/{1}'
+                              .format(iterations, iterations_count))
+        progress_bar.set_show_text(True)
+        self.vbox.pack_start(progress_bar, True, True, 0)
+
+        operation_event = {'template': ('Next {0} in {{time}} seconds...'
+                                        .format(self.pm_operation)),
+                           'timeout': pm_delay}
+        hardware_info_event = \
+            {'template': 'Gathering hardware information in {time} seconds...',
+             'timeout': hardware_delay,
+             'callback': self.on_hardware_info_timeout_cb}
+        system_info_event = \
+            {'template': 'Gathering system information in {time} seconds...',
+             'timeout': 2,
+             'callback': self.on_system_info_timeout_cb}
+
+        if iterations == 0:
+            # In first iteration, gather hardware and system  information
+            # directly and perform pm-operation
+            self.on_hardware_info_timeout_cb()
+            self.on_system_info_timeout_cb()
+            self.events = [operation_event]
+        elif iterations < iterations_count:
+            # In last iteration, wait before gathering hardware information
+            # and perform pm-operation
+            self.events = [operation_event,
+                           hardware_info_event,
+                           system_info_event]
+        else:
+            # In last iteration, wait before gathering hardware information
+            # and finish the test
+            self.events = [hardware_info_event, system_info_event]
+
+        self.label = Gtk.Label()
+        self.vbox.pack_start(self.label, True, True, 0)
+        self.show_all()
+
+    def run(self):
+        """
+        Set label text and run dialog
+        """
+        self.schedule_next_event()
+        response = super(CountdownDialog, self).run()
+        self.destroy()
+
+        if response != Gtk.ResponseType.ACCEPT:
+            raise TestCancelled()
+
+    def schedule_next_event(self):
+        """
+        Schedule next timed event
+        """
+        if self.events:
+            self.event = self.events.pop()
+            self.timeout_counter = self.event.get('timeout', 0)
+            self.label.set_text(self.event['template']
+                                .format(time=self.timeout_counter))
+            GObject.timeout_add_seconds(1, self.on_timeout_cb)
+        else:
+            # Return Accept response
+            # if there are no other events scheduled
+            self.response(Gtk.ResponseType.ACCEPT)
+
+    def on_timeout_cb(self):
+        """
+        Set label properly and use callback method if needed
+        """
+        if self.timeout_counter > 0:
+            self.label.set_text(self.event['template']
+                                .format(time=self.timeout_counter))
+            self.timeout_counter -= 1
+            return True
+
+        # Call calback if defined
+        callback = self.event.get('callback')
+        if callback:
+            callback()
+
+        # Schedule next event if needed
+        self.schedule_next_event()
+
+        return False
+
+    def on_hardware_info_timeout_cb(self):
+        """
+        Gather hardware information and print it to logs
+        """
+        logging.info('Gathering hardware information...')
+        logging.debug('Networking:\n'
+                      '{network}\n'
+                      '{ethernet}\n'
+                      '{ifconfig}\n'
+                      '{iwconfig}'
+                      .format(network=(Command('lspci | grep Network')
+                                       .run().stdout),
+                              ethernet=(Command('lspci | grep Ethernet')
+                                        .run().stdout),
+                              ifconfig=(Command(
+                                        r"ifconfig -a | grep -A1 '^\w'")
+                                        .run().stdout),
+                              iwconfig=(Command(r"iwconfig | grep -A1 '^\w'")
+                                        .run().stdout)))
+        logging.debug('Bluetooth Device:\n'
+                      '{hciconfig}'
+                      .format(hciconfig=(Command(r"hciconfig -a "
+                                                 r"| grep -A2 '^\w'")
+                                         .run().stdout)))
+        logging.debug('Video Card:\n'
+                      '{lspci}'
+                      .format(lspci=Command('lspci | grep VGA').run().stdout))
+        logging.debug('Touchpad and Keyboard:\n'
+                      '{xinput}'
+                      .format(xinput=Command(
+                          'xinput list --name-only | sort').run().stdout))
+        logging.debug('Pulse Audio Sink:\n'
+                      '{pactl_sink}'
+                      .format(pactl_sink=(Command('pactl list | grep Sink')
+                                          .run().stdout)))
+        logging.debug('Pulse Audio Source:\n'
+                      '{pactl_source}'
+                      .format(pactl_source=(Command('pactl list | grep Source')
+                                            .run().stdout)))
+
+        # Check kernel logs using firmware test suite
+        command = Command('fwts -r stdout klog oops').run()
+        if command.process.returncode != 0:
+            # Don't abort the test loop,
+            # errors can be retrieved by pm_log_check.py
+            logging.error('Problem found in logs by fwts')
+
+    def on_system_info_timeout_cb(self):
+        """
+        Gather system information and print it to logs
+        """
+        logging.info('Gathering system information...')
+        # FIXME: Commented out as it created huge log files
+        # during stress tests.
+        # logging.debug('--- beginning of dmesg ---')
+        # logging.debug(Command('dmesg').run().stdout)
+        # logging.debug('--- end of dmesg ---')
+        # logging.debug('--- beginning of syslog ---')
+        # logging.debug(Command('cat /var/log/syslog').run().stdout)
+        # logging.debug('--- end of syslog ---')
+
+
+class MessageDialog():
+    """
+    Simple wrapper aroung Gtk.MessageDialog
+    """
+    def __init__(self, title, message, type=Gtk.MessageType.INFO):
+        self.title = title
+        self.message = message
+        self.type = type
+
+    def run(self):
+        dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.OK,
+                                   message_format=self.message,
+                                   type=self.type)
+        logging.info(self.message)
+        dialog.set_title(self.title)
+        dialog.run()
+        dialog.destroy()
+
+
+class AutoLoginConfigurator():
+    """
+    Enable/disable autologin configuration
+    to make sure that reboot test will work properly
+    """
+    def __init__(self, user=None):
+        self.user = user
+        self.config_filename = '/etc/lightdm/lightdm.conf'
+        self.template = """
+[SeatDefaults]
+greeter-session=unity-greeter
+user-session=ubuntu
+autologin-user={username}
+autologin-user-timeout=0
+"""
+        if os.path.exists('/etc/gdm3/custom.conf'):
+            self.config_filename = '/etc/gdm3/custom.conf'
+            self.parser = ConfigParser()
+            self.parser.optionxform = str
+            self.parser.read(self.config_filename)
+            self.parser.set('daemon', 'AutomaticLoginEnable', 'True')
+            if self.user:
+                self.parser.set('daemon', 'AutomaticLogin', self.user)
+
+    def enable(self):
+        """
+        Make sure user will autologin in next reboot
+        """
+        logging.debug('Enabling autologin for this user...')
+        if os.path.exists(self.config_filename):
+            for backup_filename in self.generate_backup_filename():
+                if not os.path.exists(backup_filename):
+                    shutil.copyfile(self.config_filename, backup_filename)
+                    shutil.copystat(self.config_filename, backup_filename)
+                    break
+
+        with open(self.config_filename, 'w') as f:
+            if self.config_filename == '/etc/lightdm/lightdm.conf':
+                f.write(self.template.format(username=self.user))
+            elif self.config_filename == '/etc/gdm3/custom.conf':
+                self.parser.write(f)
+
+    def disable(self):
+        """
+        Remove latest configuration file
+        and use the same configuration that was in place
+        before running the test
+        """
+        logging.debug('Restoring autologin configuration...')
+        backup_filename = None
+        for filename in self.generate_backup_filename():
+            if not os.path.exists(filename):
+                break
+            backup_filename = filename
+
+        if backup_filename:
+            shutil.copy(backup_filename, self.config_filename)
+            os.remove(backup_filename)
+        else:
+            os.remove(self.config_filename)
+
+    def generate_backup_filename(self):
+        backup_filename = self.config_filename + '.bak'
+        yield backup_filename
+
+        index = 0
+        while True:
+            index += 1
+            backup_filename = (self.config_filename +
+                               '.bak.{0}'.format(index))
+            yield backup_filename
+
+
+class SudoersConfigurator():
+    """
+    Enable/disable reboot test to be executed as root
+    to make sure that reboot test works properly
+    """
+    MARK = '# Automatically added by pm.py'
+    SUDOERS = '/etc/sudoers'
+
+    def __init__(self, user=None):
+        self.user = user
+
+    def enable(self):
+        """
+        Make sure that user will be allowed to execute reboot test as root
+        """
+        logging.debug('Enabling user to execute test as root...')
+        command = ("sed -i -e '$a{mark}\\n"
+                   "{user} ALL=NOPASSWD: /usr/bin/python3' "
+                   "{filename}".format(mark=self.MARK,
+                                       user=self.user,
+                                       script=os.path.realpath(__file__),
+                                       filename=self.SUDOERS))
+
+        Command(command, verbose=False).run()
+
+    def disable(self):
+        """
+        Revert sudoers configuration changes
+        """
+        logging.debug('Restoring sudoers configuration...')
+        command = (("sed -i -e '/{mark}/,+1d' "
+                    "{filename}")
+                   .format(mark=self.MARK,
+                           filename=self.SUDOERS))
+        Command(command, verbose=False).run()
+
+
+class AutoStartFile():
+    """
+    Generate autostart file contents and write it to proper location
+    """
+    TEMPLATE = """
+[Desktop Entry]
+Name={pm_operation} test
+Comment=Verify {pm_operation} works properly
+Exec=sudo {script} -r {repetitions} -w {wakeup} --hardware-delay {hardware_delay} --pm-delay {pm_delay} --min-pm-time {min_pm_time} --max-pm-time {max_pm_time} --append --total {total} --start {start} --pm-timestamp {pm_timestamp} {silent} --log-level={log_level} --log-dir={log_dir} --suspends-before-reboot={suspend_cycles} --checkbox-respawn-cmd={checkbox_respawn} {check_hardware} {fwts} {pm_operation}
+Type=Application
+X-GNOME-Autostart-enabled=true
+Hidden=false
+"""  # noqa: E501
+
+    def __init__(self, args, user=None):
+        self.args = args
+        self.user = user
+
+        # Generate desktop filename
+        # based on environment variables
+        username = self.user
+        default_config_directory = os.path.expanduser('~{0}/.config'
+                                                      .format(username))
+        config_directory = os.getenv('XDG_CONFIG_HOME',
+                                     default_config_directory)
+        autostart_directory = os.path.join(config_directory, 'autostart')
+        if not os.path.exists(autostart_directory):
+            os.makedirs(autostart_directory)
+            user_id = os.getenv('PKEXEC_UID') or os.getenv('SUDO_UID')
+            group_id = os.getenv('PKEXEC_UID') or os.getenv('SUDO_GID')
+            if user_id:
+                os.chown(config_directory, int(user_id), int(group_id))
+                os.chown(autostart_directory, int(user_id), int(group_id))
+
+        basename = '{0}.desktop'.format(os.path.basename(__file__))
+        self.desktop_filename = os.path.join(autostart_directory,
+                                             basename)
+
+    def write(self):
+        """
+        Write autostart file to execute the script on startup
+        """
+        logging.debug('Writing desktop file ({0!r})...'
+                      .format(self.desktop_filename))
+        snap_name = os.getenv('SNAP_NAME')
+        if snap_name:
+            script = '/snap/bin/{}.pm-test'.format(snap_name)
+        else:
+            script = '/usr/bin/python3 {}'.format(os.path.realpath(__file__))
+        contents = (self.TEMPLATE
+                    .format(script=script,
+                            repetitions=self.args.repetitions - 1,
+                            wakeup=self.args.wakeup,
+                            hardware_delay=self.args.hardware_delay,
+                            pm_delay=self.args.pm_delay,
+                            min_pm_time=self.args.min_pm_time,
+                            max_pm_time=self.args.max_pm_time,
+                            total=self.args.total,
+                            start=self.args.start,
+                            pm_timestamp=int(time()),
+                            silent='--silent' if self.args.silent else '',
+                            log_level=self.args.log_level_str,
+                            log_dir=self.args.log_dir,
+                            fwts='--fwts' if self.args.fwts else '',
+                            suspend_cycles=self.args.suspends_before_reboot,
+                            pm_operation=self.args.pm_operation,
+                            checkbox_respawn=self.args.checkbox_respawn_cmd,
+                            check_hardware='--check-hardware-list' if
+                            self.args.check_hardware_list else '',
+                            )
+                    )
+        logging.debug(contents)
+
+        with open(self.desktop_filename, 'w') as f:
+            f.write(contents)
+
+    def remove(self):
+        """
+        Remove autostart file to avoid executing the script on startup
+        """
+        if os.path.exists(self.desktop_filename):
+            logging.debug('Removing desktop file ({0!r})...'
+                          .format(self.desktop_filename))
+            os.remove(self.desktop_filename)
+
+
+class LoggingConfiguration():
+    @classmethod
+    def set(cls, log_level, log_filename, append):
+        """
+        Configure a rotating file logger
+        """
+        logger = logging.getLogger()
+        logger.setLevel(logging.DEBUG)
+
+        # Log to sys.stdout using log level passed through command line
+        if log_level != logging.NOTSET:
+            log_handler = logging.StreamHandler(sys.stdout)
+            formatter = logging.Formatter('%(levelname)-8s %(message)s')
+            log_handler.setFormatter(formatter)
+            log_handler.setLevel(log_level)
+            logger.addHandler(log_handler)
+
+        # Log to rotating file using DEBUG log level
+        log_handler = logging.handlers.RotatingFileHandler(log_filename,
+                                                           mode='a+',
+                                                           backupCount=3)
+        formatter = logging.Formatter('%(asctime)s %(levelname)-8s '
+                                      '%(message)s')
+        log_handler.setFormatter(formatter)
+        log_handler.setLevel(logging.DEBUG)
+        logger.addHandler(log_handler)
+
+        if not append:
+            # Create a new log file on every new
+            # (i.e. not scheduled) invocation
+            log_handler.doRollover()
+
+
+class MyArgumentParser():
+    """
+    Command-line argument parser
+    """
+    def __init__(self):
+        """
+        Create parser object
+        """
+        pm_operations = ('poweroff', 'reboot')
+        description = 'Run power management operation as many times as needed'
+        epilog = ('Unknown arguments will be passed '
+                  'to the underlying command: poweroff or reboot.')
+        parser = ArgumentParser(description=description, epilog=epilog)
+        parser.add_argument('-r', '--repetitions', type=int, default=1,
+                            help=('Number of times that the power management '
+                                  'operation has to be repeated '
+                                  '(%(default)s by default)'))
+        parser.add_argument('-w', '--wakeup', type=int, default=60,
+                            help=('Timeout in seconds for the wakeup alarm '
+                                  '(%(default)s by default). '
+                                  "Note: wakeup alarm won't be scheduled "
+                                  'for reboot.'))
+        parser.add_argument('--min-pm-time', dest='min_pm_time',
+                            type=int, default=0,
+                            help=('Minimum time in seconds that '
+                                  'it should take the power management '
+                                  'operation each cycle (0 for reboot and '
+                                  'wakeup time minus two seconds '
+                                  'for the other power management operations '
+                                  'by default)'))
+        parser.add_argument('--max-pm-time', dest='max_pm_time',
+                            type=int, default=300,
+                            help=('Maximum time in seconds '
+                                  'that it should take '
+                                  'the power management operation each cycle '
+                                  '(%(default)s by default)'))
+        parser.add_argument('--pm-delay', dest='pm_delay',
+                            type=int, default=5,
+                            help=('Delay in seconds '
+                                  'after hardware information '
+                                  'has been gathered and before executing '
+                                  'the power management operation '
+                                  '(%(default)s by default)'))
+        parser.add_argument('--hardware-delay', dest='hardware_delay',
+                            type=int, default=30,
+                            help=('Delay in seconds before gathering hardware '
+                                  'information (%(default)s by default)'))
+        parser.add_argument('--silent', action='store_true',
+                            help=("Don't display any dialog "
+                                  'when test is complete '
+                                  'to let the script be used '
+                                  'in automated tests'))
+        log_levels = ['notset', 'debug', 'info',
+                      'warning', 'error', 'critical']
+        parser.add_argument('--log-level', dest='log_level_str',
+                            default='info', choices=log_levels,
+                            help=('Log level. '
+                                  'One of {0} or {1} (%(default)s by default)'
+                                  .format(', '.join(log_levels[:-1]),
+                                          log_levels[-1])))
+        parser.add_argument('--log-dir', dest='log_dir', default='/var/log',
+                            help=('Path to the directory to store log files'))
+        parser.add_argument('pm_operation', choices=pm_operations,
+                            help=('Power management operation to be performed '
+                                  '(one of {0} or {1!r})'
+                                  .format(', '.join(map(repr,
+                                                        pm_operations[:-1])),
+                                          pm_operations[-1])))
+
+        # Test timestamps
+        parser.add_argument('--start', type=int, default=0, help=SUPPRESS)
+        parser.add_argument('--pm-timestamp', dest='pm_timestamp',
+                            type=int, default=0, help=SUPPRESS)
+
+        # Append to log on subsequent startups
+        parser.add_argument('--append', action='store_true',
+                            default=False, help=SUPPRESS)
+
+        # Total number of iterations initially passed through the command line
+        parser.add_argument('--total', type=int, default=0, help=SUPPRESS)
+
+        # suspend cycles before reboot
+        parser.add_argument('--suspends-before-reboot', type=int, default=0,
+                            help=('How many cycles of suspend/resume to run'
+                                  'before each reboot or poweroff'))
+
+        # use fwts for suspend tests
+        parser.add_argument('--fwts', action='store_true', help=('Use fwts '
+                            'when doing the suspend tests'))
+        parser.add_argument('--checkbox-respawn-cmd', type=str, help=(
+            'path to a file telling how to return to checkbox after the'
+            ' test is done'), default='')
+        parser.add_argument('--check-hardware-list', action='store_true',
+                            help=('Look for changes in the list of devices '
+                                  'after each PM action'), default=False)
+        self.parser = parser
+
+    def parse(self):
+        """
+        Parse command-line arguments
+        """
+        args, extra_args = self.parser.parse_known_args()
+        args.log_level = getattr(logging, args.log_level_str.upper())
+
+        # Total number of repetitions
+        # is the number of repetitions passed through the command line
+        # the first time the script is executed
+        if not args.total:
+            args.total = args.repetitions
+
+        # Test start time automatically set on first iteration
+        if not args.start:
+            args.start = int(time())
+
+        # Wakeup time set to 0 for 'reboot'
+        # since wakeup alarm won't be scheduled
+        if args.pm_operation == 'reboot':
+            args.wakeup = 0
+            args.min_pm_time = 0
+
+        # Minimum time for each power management operation
+        # is set to the wakeup time
+        if not args.min_pm_time:
+            min_pm_time = args.wakeup - 2
+            if min_pm_time < 0:
+                min_pm_time = 0
+            args.min_pm_time = min_pm_time
+
+        # Log filename shows clearly the type of test (pm_operation)
+        # and the times it was repeated (repetitions)
+        args.log_filename = os.path.join(
+            args.log_dir,
+            '{0}.{1}.{2}.log'.format(
+                os.path.splitext(os.path.basename(__file__))[0],
+                args.pm_operation,
+                args.total))
+        return args, extra_args
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/removable_storage_test.py b/org.oniroproject.integration-tests/bin/removable_storage_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..4dcd5b2247b14ab55bb07a5b2d71eb69009214a2
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/removable_storage_test.py
@@ -0,0 +1,905 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import dbus
+import hashlib
+import logging
+import os
+import platform
+import re
+import shlex
+import subprocess
+import sys
+import tempfile
+import time
+
+import gi
+gi.require_version('GUdev', '1.0')
+from gi.repository import GUdev                                 # noqa: E402
+
+from checkbox_support.dbus import connect_to_system_bus         # noqa: E402
+from checkbox_support.dbus.udisks2 import (
+    UDISKS2_BLOCK_INTERFACE,
+    UDISKS2_DRIVE_INTERFACE,
+    UDISKS2_FILESYSTEM_INTERFACE,
+    UDISKS2_LOOP_INTERFACE,
+    UDisks2Model,
+    UDisks2Observer,
+    is_udisks2_supported,
+    lookup_udev_device,
+    map_udisks1_connection_bus)                                 # noqa: E402
+from checkbox_support.heuristics.udisks2 import is_memory_card  # noqa: E402
+from checkbox_support.helpers.human_readable_bytes import (
+    HumanReadableBytes)                                         # noqa: E402
+from checkbox_support.parsers.udevadm import (
+    CARD_READER_RE,
+    GENERIC_RE,
+    FLASH_RE,
+    find_pkname_is_root_mountpoint)                             # noqa: E402
+from checkbox_support.udev import get_interconnect_speed        # noqa: E402
+from checkbox_support.udev import get_udev_block_devices        # noqa: E402
+from checkbox_support.udev import get_udev_xhci_devices         # noqa: E402
+
+
+class ActionTimer():
+    '''Class to implement a simple timer'''
+
+    def __enter__(self):
+        self.start = time.time()
+        return self
+
+    def __exit__(self, *args):
+        self.stop = time.time()
+        self.interval = self.stop - self.start
+
+
+class RandomData():
+    '''Class to create data files'''
+
+    def __init__(self, size):
+        self.tfile = tempfile.NamedTemporaryFile(delete=False)
+        self.path = ''
+        self.name = ''
+        self.path, self.name = os.path.split(self.tfile.name)
+        self._write_test_data_file(size)
+
+    def _generate_test_data(self):
+        seed = "104872948765827105728492766217823438120"
+        phrase = '''
+        Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam
+        nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat
+        volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation
+        ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
+        Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
+        molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero
+        eros et accumsan et iusto odio dignissim qui blandit praesent luptatum
+        zzril delenit augue duis dolore te feugait nulla facilisi.
+        '''
+        words = phrase.replace('\n', '').split()
+        word_deque = collections.deque(words)
+        seed_deque = collections.deque(seed)
+        while True:
+            yield ' '.join(list(word_deque))
+            word_deque.rotate(int(seed_deque[0]))
+            seed_deque.rotate(1)
+
+    def _write_test_data_file(self, size):
+        data = self._generate_test_data()
+        while os.path.getsize(self.tfile.name) < size:
+            self.tfile.write(next(data).encode('UTF-8'))
+        return self
+
+
+def md5_hash_file(path):
+    md5 = hashlib.md5()
+    try:
+        with open(path, 'rb') as stream:
+            while True:
+                data = stream.read(8192)
+                if not data:
+                    break
+                md5.update(data)
+    except IOError as exc:
+        logging.error("unable to checksum %s: %s", path, exc)
+        return None
+    else:
+        return md5.hexdigest()
+
+
+def on_ubuntucore():
+    """
+    Check if running from on ubuntu core
+    """
+    snap = os.getenv("SNAP")
+    if snap:
+        with open(os.path.join(snap, 'meta/snap.yaml')) as f:
+            for line in f.readlines():
+                if line == "confinement: classic\n":
+                    return False
+        return True
+    return False
+
+
+class DiskTest():
+    ''' Class to contain various methods for testing removable disks '''
+
+    def __init__(self, device, memorycard, lsblkcommand):
+        self.rem_disks = {}     # mounted before the script running
+        self.rem_disks_nm = {}  # not mounted before the script running
+        self.rem_disks_memory_cards = {}
+        self.rem_disks_memory_cards_nm = {}
+        self.rem_disks_speed = {}
+        # LP: #1313581, TODO: extend to be rem_disks_driver
+        self.rem_disks_xhci = {}
+        self.data = ''
+        self.lsblk = ''
+        self.device = device
+        self.memorycard = memorycard
+        self._run_lsblk(lsblkcommand)
+        self._probe_disks()
+
+    def read_file(self, source):
+        with open(source, 'rb') as infile:
+            try:
+                self.data = infile.read()
+            except IOError as exc:
+                logging.error("Unable to read data from %s: %s", source, exc)
+                return False
+            else:
+                return True
+
+    def write_file(self, data, dest):
+        try:
+            outfile = open(dest, 'wb', 0)
+        except OSError as exc:
+            logging.error("Unable to open %s for writing.", dest)
+            logging.error("  %s", exc)
+            return False
+        with outfile:
+            try:
+                outfile.write(self.data)
+            except IOError as exc:
+                logging.error("Unable to write data to %s: %s", dest, exc)
+                return False
+            else:
+                outfile.flush()
+                os.fsync(outfile.fileno())
+                return True
+
+    def clean_up(self, target):
+        try:
+            os.unlink(target)
+        except OSError as exc:
+            logging.error("Unable to remove tempfile %s", target)
+            logging.error("  %s", exc)
+
+    def _find_parent(self, device):
+        if self.lsblk:
+            pattern = re.compile('KNAME="(?P<KNAME>.*)" '
+                                 'TYPE="(?P<TYPE>.*)" '
+                                 'MOUNTPOINT="(?P<MOUNTPOINT>.*)"')
+            for line in self.lsblk.splitlines():
+                m = pattern.match(line)
+                if m and device.startswith(m.group('KNAME')):
+                    return m.group('KNAME')
+        return False
+
+    def _run_lsblk(self, lsblkcommand):
+        try:
+            self.lsblk = subprocess.check_output(shlex.split(lsblkcommand),
+                                                 universal_newlines=True)
+        except subprocess.CalledProcessError as exc:
+            raise SystemExit(exc)
+
+    def _probe_disks(self):
+        """
+        Internal method used to probe for available disks
+
+        Indirectly sets:
+            self.rem_disks{,_nm,_memory_cards,_memory_cards_nm,_speed}
+        """
+        if on_ubuntucore():
+            self._probe_disks_udisks2_cli()
+        else:
+            bus, loop = connect_to_system_bus()
+            if is_udisks2_supported(bus):
+                self._probe_disks_udisks2(bus)
+            else:
+                self._probe_disks_udisks1(bus)
+
+    def _probe_disks_udisks2_cli(self):
+        # First we will build up a db of udisks info by scraping the output
+        # of the dump command
+        # TODO: remove the snap prefix when the alias becomes available
+        proc = subprocess.Popen(['udisks2.udisksctl', 'dump'],
+                                stdout=subprocess.PIPE)
+        udisks_devices = {}
+        current_bd = None
+        current_interface = None
+        while True:
+            line = proc.stdout.readline().decode(sys.stdout.encoding)
+            if line == '':
+                break
+            if line == '\n':
+                current_bd = None
+                current_interface = None
+            if line.startswith('/org/freedesktop/UDisks2/'):
+                path = line.strip()
+                current_bd = os.path.basename(path).rstrip(':')
+                udisks_devices[current_bd] = {}
+                continue
+            if current_bd is None:
+                continue
+            if line.startswith('  org.freedesktop'):
+                current_interface = line.strip().rstrip(':')
+                udisks_devices[current_bd][current_interface] = {}
+                continue
+            if current_interface is None:
+                continue
+            entry = ''.join(c for c in line if c not in '\n\t\' ')
+            wanted_keys = ('Device:', 'Drive:', 'MountPoints:', 'Vendor:',
+                           'ConnectionBus:', 'Model:', 'Media:',)
+            for key in wanted_keys:
+                if entry.startswith(key):
+                    udisks_devices[current_bd][current_interface][key] = (
+                        entry[len(key):])
+
+        # Now use the populated udisks structure to fill out the API used by
+        # other _probe disks functions
+        for device, interfaces in udisks_devices.items():
+            # iterate over udisks objects that have both filesystem and
+            # block device interfaces
+            if (UDISKS2_FILESYSTEM_INTERFACE in interfaces and
+                    UDISKS2_BLOCK_INTERFACE in interfaces):
+                # To be an IO candidate there must be a drive object
+                drive = interfaces[UDISKS2_BLOCK_INTERFACE].get('Drive:')
+                if drive is None or drive == '/':
+                    continue
+                drive_object = udisks_devices[os.path.basename(drive)]
+
+                # Get the connection bus property from the drive interface of
+                # the drive object. This is required to filter out the devices
+                # we don't want to look at now.
+                connection_bus = (
+                    drive_object[UDISKS2_DRIVE_INTERFACE]['ConnectionBus:'])
+                desired_connection_buses = set([
+                    map_udisks1_connection_bus(device)
+                    for device in self.device])
+                # Skip devices that are attached to undesired connection buses
+                if connection_bus not in desired_connection_buses:
+                    continue
+
+                dev_file = (
+                    interfaces[UDISKS2_BLOCK_INTERFACE].get('Device:'))
+
+                parent = self._find_parent(dev_file.replace('/dev/', ''))
+                if (parent and
+                        find_pkname_is_root_mountpoint(parent, self.lsblk)):
+                    continue
+
+                # XXX: we actually only scrape the first one currently
+                mount_point = (
+                    interfaces[UDISKS2_FILESYSTEM_INTERFACE].get(
+                        'MountPoints:'))
+                if mount_point == '':
+                    mount_point = None
+
+                # We need to skip-non memory cards if we look for memory cards
+                # and vice-versa so let's inspect the drive and use heuristics
+                # to detect memory cards (a memory card reader actually) now.
+                if self.memorycard != is_memory_card(
+                        drive_object[UDISKS2_DRIVE_INTERFACE]['Vendor:'],
+                        drive_object[UDISKS2_DRIVE_INTERFACE]['Model:'],
+                        drive_object[UDISKS2_DRIVE_INTERFACE]['Media:']):
+                    continue
+
+                if mount_point is None:
+                    self.rem_disks_memory_cards_nm[dev_file] = None
+                    self.rem_disks_nm[dev_file] = None
+                else:
+                    self.rem_disks_memory_cards[dev_file] = mount_point
+                    self.rem_disks[dev_file] = mount_point
+
+                # Get the speed of the interconnect that is associated with the
+                # block device we're looking at. This is purely informational
+                # but it is a part of the required API
+                udev_devices = get_udev_block_devices(GUdev.Client())
+                for udev_device in udev_devices:
+                    if udev_device.get_device_file() == dev_file:
+                        interconnect_speed = get_interconnect_speed(
+                            udev_device)
+                        if interconnect_speed:
+                            self.rem_disks_speed[dev_file] = (
+                                interconnect_speed * 10 ** 6)
+                        else:
+                            self.rem_disks_speed[dev_file] = None
+
+    def _probe_disks_udisks2(self, bus):
+        """
+        Internal method used to probe / discover available disks using udisks2
+        dbus interface using the provided dbus bus (presumably the system bus)
+        """
+        # We'll need udisks2 and udev to get the data we need
+        udisks2_observer = UDisks2Observer()
+        udisks2_model = UDisks2Model(udisks2_observer)
+        udisks2_observer.connect_to_bus(bus)
+        udev_client = GUdev.Client()
+        # Get a collection of all udev devices corresponding to block devices
+        udev_devices = get_udev_block_devices(udev_client)
+        # Get a collection of all udisks2 objects
+        udisks2_objects = udisks2_model.managed_objects
+        # Let's get a helper to simplify the loop below
+
+        def iter_filesystems_on_block_devices():
+            """
+            Generate a collection of UDisks2 object paths that
+            have both the filesystem and block device interfaces
+            """
+            for udisks2_object_path, interfaces in udisks2_objects.items():
+                if (UDISKS2_FILESYSTEM_INTERFACE in interfaces and
+                        UDISKS2_BLOCK_INTERFACE in interfaces and
+                        UDISKS2_LOOP_INTERFACE not in interfaces):
+                    yield udisks2_object_path
+        # We need to know about all IO candidates,
+        # let's iterate over all the block devices reported by udisks2
+        for udisks2_object_path in iter_filesystems_on_block_devices():
+            # Get interfaces implemented by this object
+            udisks2_object = udisks2_objects[udisks2_object_path]
+            # Find the path of the udisks2 object that represents the drive
+            # this object is a part of
+            drive_object_path = (
+                udisks2_object[UDISKS2_BLOCK_INTERFACE]['Drive'])
+            # Lookup the drive object, if any. This can fail when
+            try:
+                drive_object = udisks2_objects[drive_object_path]
+            except KeyError:
+                logging.error(
+                    "Unable to locate drive associated with %s",
+                    udisks2_object_path)
+                continue
+            else:
+                drive_props = drive_object[UDISKS2_DRIVE_INTERFACE]
+            # Get the connection bus property from the drive interface of the
+            # drive object. This is required to filter out the devices we don't
+            # want to look at now.
+            connection_bus = drive_props["ConnectionBus"]
+            desired_connection_buses = set([
+                map_udisks1_connection_bus(device)
+                for device in self.device])
+            # Skip devices that are attached to undesired connection buses
+            if connection_bus not in desired_connection_buses:
+                continue
+            # Lookup the udev object that corresponds to this object
+            try:
+                udev_device = lookup_udev_device(udisks2_object, udev_devices)
+            except LookupError:
+                logging.error(
+                    "Unable to locate udev object that corresponds to: %s",
+                    udisks2_object_path)
+                continue
+            # Get the block device pathname,
+            # to avoid the confusion, this is something like /dev/sdbX
+            dev_file = udev_device.get_device_file()
+            parent = self._find_parent(dev_file.replace('/dev/', ''))
+            if parent and find_pkname_is_root_mountpoint(parent, self.lsblk):
+                continue
+            # Get the list of mount points of this block device
+            mount_points = (
+                udisks2_object[UDISKS2_FILESYSTEM_INTERFACE]['MountPoints'])
+            # Get the speed of the interconnect that is associated with the
+            # block device we're looking at. This is purely informational but
+            # it is a part of the required API
+            interconnect_speed = get_interconnect_speed(udev_device)
+            if interconnect_speed:
+                self.rem_disks_speed[dev_file] = (
+                    interconnect_speed * 10 ** 6)
+            else:
+                self.rem_disks_speed[dev_file] = None
+            # Ensure it is a media card reader if this was explicitly requested
+            drive_is_reader = is_memory_card(
+                drive_props['Vendor'], drive_props['Model'],
+                drive_props['Media'])
+            if self.memorycard and not drive_is_reader:
+                continue
+            # The if/else test below simply distributes the mount_point to the
+            # appropriate variable, to keep the API requirements. It is
+            # confusing as _memory_cards is variable is somewhat dummy.
+            if mount_points:
+                # XXX: Arbitrarily pick the first of the mount points
+                mount_point = mount_points[0]
+                self.rem_disks_memory_cards[dev_file] = mount_point
+                self.rem_disks[dev_file] = mount_point
+            else:
+                self.rem_disks_memory_cards_nm[dev_file] = None
+                self.rem_disks_nm[dev_file] = None
+
+    def _probe_disks_udisks1(self, bus):
+        """
+        Internal method used to probe / discover available disks using udisks1
+        dbus interface using the provided dbus bus (presumably the system bus)
+        """
+        ud_manager_obj = bus.get_object("org.freedesktop.UDisks",
+                                        "/org/freedesktop/UDisks")
+        ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks')
+        for dev in ud_manager.EnumerateDevices():
+            device_obj = bus.get_object("org.freedesktop.UDisks", dev)
+            device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE)
+            udisks = 'org.freedesktop.UDisks.Device'
+            if not device_props.Get(udisks, "DeviceIsDrive"):
+                dev_bus = device_props.Get(udisks, "DriveConnectionInterface")
+                if dev_bus in self.device:
+                    parent_model = parent_vendor = ''
+                    if device_props.Get(udisks, "DeviceIsPartition"):
+                        parent_obj = bus.get_object(
+                            "org.freedesktop.UDisks",
+                            device_props.Get(udisks, "PartitionSlave"))
+                        parent_props = dbus.Interface(
+                            parent_obj, dbus.PROPERTIES_IFACE)
+                        parent_model = parent_props.Get(udisks, "DriveModel")
+                        parent_vendor = parent_props.Get(udisks, "DriveVendor")
+                        parent_media = parent_props.Get(udisks, "DriveMedia")
+                    if self.memorycard:
+                        if (dev_bus != 'sdio' and not
+                                FLASH_RE.search(parent_media) and not
+                                CARD_READER_RE.search(parent_model) and not
+                                GENERIC_RE.search(parent_vendor)):
+                            continue
+                    else:
+                        if (FLASH_RE.search(parent_media) or
+                                CARD_READER_RE.search(parent_model) or
+                                GENERIC_RE.search(parent_vendor)):
+                            continue
+                    dev_file = str(device_props.Get(udisks, "DeviceFile"))
+                    dev_speed = str(device_props.Get(udisks,
+                                                     "DriveConnectionSpeed"))
+                    self.rem_disks_speed[dev_file] = dev_speed
+                    if len(device_props.Get(udisks, "DeviceMountPaths")) > 0:
+                        devPath = str(device_props.Get(udisks,
+                                                       "DeviceMountPaths")[0])
+                        self.rem_disks[dev_file] = devPath
+                        self.rem_disks_memory_cards[dev_file] = devPath
+                    else:
+                        self.rem_disks_nm[dev_file] = None
+                        self.rem_disks_memory_cards_nm[dev_file] = None
+
+    def get_disks_xhci(self):
+        """
+        Compare
+        1. the pci slot name of the devices using xhci
+        2. the pci slot name of the disks,
+           which is usb3 disks in this case so far,
+        to make sure the usb3 disk does be on the controller using xhci
+        """
+        # LP: #1378724
+        udev_client = GUdev.Client()
+        # Get a collection of all udev devices corresponding to block devices
+        udev_devices = get_udev_block_devices(udev_client)
+        # Get a collection of all udev devices corresponding to xhci devices
+        udev_devices_xhci = get_udev_xhci_devices(udev_client)
+        if platform.machine() in ("aarch64", "armv7l"):
+            enumerator = GUdev.Enumerator(client=udev_client)
+            udev_devices_xhci = [
+                device for device in enumerator.execute()
+                if (device.get_driver() == 'xhci-hcd' or
+                    device.get_driver() == 'xhci_hcd')]
+        for udev_device_xhci in udev_devices_xhci:
+            pci_slot_name = udev_device_xhci.get_property('PCI_SLOT_NAME')
+            xhci_devpath = udev_device_xhci.get_property('DEVPATH')
+            for udev_device in udev_devices:
+                devpath = udev_device.get_property('DEVPATH')
+                if (self._compare_pci_slot_from_devpath(devpath,
+                                                        pci_slot_name)):
+                    self.rem_disks_xhci[
+                        udev_device.get_property('DEVNAME')] = 'xhci'
+                if platform.machine() in ("aarch64", "armv7l"):
+                    if xhci_devpath in devpath:
+                        self.rem_disks_xhci[
+                            udev_device.get_property('DEVNAME')] = 'xhci'
+        return self.rem_disks_xhci
+
+    def mount(self):
+        passed_mount = {}
+
+        for key in self.rem_disks_nm:
+            temp_dir = tempfile.mkdtemp()
+            if self._mount(key, temp_dir) != 0:
+                logging.error("can't mount %s", key)
+            else:
+                passed_mount[key] = temp_dir
+
+        if len(self.rem_disks_nm) == len(passed_mount):
+            self.rem_disks_nm = passed_mount
+            return 0
+        else:
+            count = len(self.rem_disks_nm) - len(passed_mount)
+            self.rem_disks_nm = passed_mount
+            return count
+
+    def _mount(self, dev_file, mount_point):
+        return subprocess.call(['mount', dev_file, mount_point])
+
+    def umount(self):
+        errors = 0
+        for disk in self.rem_disks_nm:
+            if not self.rem_disks_nm[disk]:
+                continue
+            if self._umount(disk) != 0:
+                errors += 1
+                logging.error("can't umount %s on %s",
+                              disk, self.rem_disks_nm[disk])
+        return errors
+
+    def _umount(self, mount_point):
+        # '-l': lazy umount, dealing problem of unable to umount the device.
+        return subprocess.call(['umount', '-l', mount_point])
+
+    def clean_tmp_dir(self):
+        for disk in self.rem_disks_nm:
+            if not self.rem_disks_nm[disk]:
+                continue
+            if not os.path.ismount(self.rem_disks_nm[disk]):
+                os.rmdir(self.rem_disks_nm[disk])
+
+    def _compare_pci_slot_from_devpath(self, devpath, pci_slot_name):
+        # LP: #1334991
+        # a smarter parser to get and validate a pci slot name from DEVPATH
+        # then compare this pci slot name to the other
+        dl = devpath.split('/')
+        s = set([x for x in dl if dl.count(x) > 1])
+        if (
+            (pci_slot_name in dl) and
+            (dl.index(pci_slot_name) < dl.index('block')) and
+            (not(pci_slot_name in s))
+        ):
+            # 1. there is such pci_slot_name
+            # 2. sysfs topology looks like
+            #    DEVPATH = ....../pci_slot_name/....../block/......
+            # 3. pci_slot_name should be unique in DEVPATH
+            return True
+        else:
+            return False
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('device',
+                        choices=['usb', 'firewire', 'sdio',
+                                 'scsi', 'ata_serial_esata'],
+                        nargs='+',
+                        help=("The type of removable media "
+                              "(usb, firewire, sdio, scsi or ata_serial_esata)"
+                              "to test."))
+    parser.add_argument('-l', '--list',
+                        action='store_true',
+                        default=False,
+                        help="List the removable devices and mounting status")
+    parser.add_argument('-m', '--min-speed',
+                        action='store',
+                        default=0,
+                        type=int,
+                        help="Minimum speed a device must support to be "
+                             "considered eligible for being tested (bits/s)")
+    parser.add_argument('-p', '--pass-speed',
+                        action='store',
+                        default=0,
+                        type=int,
+                        help="Minimum average throughput from all eligible"
+                             "devices for the test to pass (MB/s)")
+    parser.add_argument('-i', '--iterations',
+                        action='store',
+                        default='1',
+                        type=int,
+                        help=("The number of test cycles to run. One cycle is"
+                              "comprised of generating --count data files of "
+                              "--size bytes and writing them to each device."))
+    parser.add_argument('-c', '--count',
+                        action='store',
+                        default='1',
+                        type=int,
+                        help='The number of random data files to generate')
+    parser.add_argument('-s', '--size',
+                        action='store',
+                        type=HumanReadableBytes,
+                        default='1MiB',
+                        help=("The size of the test data file to use. "
+                              "You may use SI or IEC suffixes like: 'K', 'M',"
+                              "'G', 'T', 'Ki', 'Mi', 'Gi', 'Ti', etc. Default"
+                              " is %(default)s"))
+    parser.add_argument('--auto-reduce-size',
+                        action='store_true',
+                        default=False,
+                        help=("Automatically reduce size to fit in the target"
+                              "filesystem. Reducing until fits in 1MiB"))
+    parser.add_argument('-n', '--skip-not-mount',
+                        action='store_true',
+                        default=False,
+                        help=("skip the removable devices "
+                              "which haven't been mounted before the test."))
+    parser.add_argument('--memorycard', action="store_true",
+                        help=("Memory cards devices on bus other than sdio "
+                              "require this parameter to identify "
+                              "them as such"))
+    parser.add_argument('--driver',
+                        choices=['xhci_hcd'],
+                        help=("Detect the driver of the host controller."
+                              "Only xhci_hcd for usb3 is supported so far."))
+    parser.add_argument("--lsblkcommand", action='store', type=str,
+                        default="lsblk -i -n -P -e 7 -o KNAME,TYPE,MOUNTPOINT",
+                        help=("Command to execute to get lsblk information. "
+                              "Only change it if you know what you're doing."))
+
+    args = parser.parse_args()
+
+    test = DiskTest(args.device, args.memorycard, args.lsblkcommand)
+
+    # LP:1876966
+    if os.getuid() != 0:
+        print("ERROR: This script must be run as root!")
+        return 1
+
+    errors = 0
+    # If we do have removable drives attached and mounted
+    if len(test.rem_disks) > 0 or len(test.rem_disks_nm) > 0:
+        if args.list:  # Simply output a list of drives detected
+            print('-' * 20)
+            print("Removable devices currently mounted:")
+            if args.memorycard:
+                if len(test.rem_disks_memory_cards) > 0:
+                    for disk, mnt_point in test.rem_disks_memory_cards.items():
+                        print("%s : %s" % (disk, mnt_point))
+                else:
+                    print("None")
+
+                print("Removable devices currently not mounted:")
+                if len(test.rem_disks_memory_cards_nm) > 0:
+                    for disk in test.rem_disks_memory_cards_nm:
+                        print(disk)
+                else:
+                    print("None")
+            else:
+                if len(test.rem_disks) > 0:
+                    for disk, mnt_point in test.rem_disks.items():
+                        print("%s : %s" % (disk, mnt_point))
+                else:
+                    print("None")
+
+                print("Removable devices currently not mounted:")
+                if len(test.rem_disks_nm) > 0:
+                    for disk in test.rem_disks_nm:
+                        print(disk)
+                else:
+                    print("None")
+
+            print('-' * 20)
+
+            return 0
+
+        else:  # Create a file, copy to disk and compare hashes
+            if args.skip_not_mount:
+                disks_all = test.rem_disks
+            else:
+                # mount those haven't be mounted yet.
+                errors_mount = test.mount()
+
+                if errors_mount > 0:
+                    print("There're total %d device(s) failed at mounting."
+                          % errors_mount)
+                    errors += errors_mount
+
+                disks_all = dict(list(test.rem_disks.items()) +
+                                 list(test.rem_disks_nm.items()))
+
+            if len(disks_all) > 0:
+                print("Found the following mounted %s partitions:"
+                      % ', '.join(args.device))
+
+                for disk, mount_point in disks_all.items():
+                    supported_speed = test.rem_disks_speed[disk]
+                    print("    %s : %s : %s bits/s" %
+                          (disk, mount_point, supported_speed),
+                          end="")
+                    if (args.min_speed and
+                            int(args.min_speed) > int(supported_speed)):
+                        print(" (Will not test it, speed is below %s bits/s)" %
+                              args.min_speed, end="")
+
+                    print("")
+
+                print('-' * 20)
+
+                disks_eligible = {disk: disks_all[disk] for disk in disks_all
+                                  if not args.min_speed or
+                                  int(test.rem_disks_speed[disk]) >=
+                                  int(args.min_speed)}
+                if len(disks_eligible) == 0:
+                    logging.error(
+                        "No %s disks with speed higher than %s bits/s",
+                        args.device, args.min_speed)
+                    return 1
+                write_sizes = []
+                test_files = {}
+                disks_freespace = {}
+                for disk, path in disks_eligible.items():
+                    stat = os.statvfs(path)
+                    disks_freespace[disk] = stat.f_bfree * stat.f_bsize
+                smallest_freespace = min(disks_freespace.values())
+                smallest_partition = [d for d, v in disks_freespace.items() if
+                                      v == smallest_freespace][0]
+                desired_size = args.size
+                if desired_size > smallest_freespace:
+                    if args.auto_reduce_size:
+                        min_space = HumanReadableBytes("1MiB")
+                        if smallest_freespace < min_space:
+                            sys.exit("Not enough space. {} is required on {}"
+                                     .format(min_space, smallest_partition))
+                        new_size = HumanReadableBytes(
+                            int(0.8 * smallest_freespace))
+                        logging.warning("Automatically reducing test data size"
+                                        ". {} requested. Reducing to {}."
+                                        .format(desired_size, new_size))
+                        desired_size = new_size
+                    else:
+                        sys.exit("Not enough space. {} is required on {}"
+                                 .format(desired_size, smallest_partition))
+                # Generate our data file(s)
+                for count in range(args.count):
+                    test_files[count] = RandomData(desired_size)
+                    write_sizes.append(os.path.getsize(
+                        test_files[count].tfile.name))
+                    total_write_size = sum(write_sizes)
+
+                try:
+                    # Clear dmesg so we can check for I/O errors later
+                    subprocess.check_output(['dmesg', '-C'])
+                    for disk, mount_point in disks_eligible.items():
+                        print("%s (Total Data Size / iteration: %0.4f MB):" %
+                              (disk, (total_write_size / 1024 / 1024)))
+                        iteration_write_size = (
+                            total_write_size * args.iterations) / 1024 / 1024
+                        iteration_write_times = []
+                        for iteration in range(args.iterations):
+                            target_file_list = []
+                            write_times = []
+                            for file_index in range(args.count):
+                                parent_file = test_files[file_index].tfile.name
+                                parent_hash = md5_hash_file(parent_file)
+                                target_filename = (
+                                    test_files[file_index].name +
+                                    '.%s' % iteration)
+                                target_path = mount_point
+                                target_file = os.path.join(target_path,
+                                                           target_filename)
+                                target_file_list.append(target_file)
+                                test.read_file(parent_file)
+                                with ActionTimer() as timer:
+                                    if not test.write_file(test.data,
+                                                           target_file):
+                                        logging.error(
+                                            "Failed to copy %s to %s",
+                                            parent_file, target_file)
+                                        errors += 1
+                                        continue
+                                write_times.append(timer.interval)
+                                child_hash = md5_hash_file(target_file)
+                                if parent_hash != child_hash:
+                                    logging.warning(
+                                        "[Iteration %s] Parent and Child"
+                                        " copy hashes mismatch on %s!",
+                                        iteration, target_file)
+                                    logging.warning(
+                                        "\tParent hash: %s", parent_hash)
+                                    logging.warning(
+                                        "\tChild hash: %s", child_hash)
+                                    errors += 1
+                            for file in target_file_list:
+                                test.clean_up(file)
+                            total_write_time = sum(write_times)
+                            # avg_write_time = total_write_time / args.count
+                            try:
+                                avg_write_speed = ((
+                                    total_write_size / total_write_time) /
+                                    1024 / 1024)
+                            except ZeroDivisionError:
+                                avg_write_speed = 0.00
+                            finally:
+                                iteration_write_times.append(total_write_time)
+                                print("\t[Iteration %s] Average Speed: %0.4f"
+                                      % (iteration, avg_write_speed))
+                        for iteration in range(args.iterations):
+                            iteration_write_time = sum(iteration_write_times)
+                        print("\tSummary:")
+                        print("\t\tTotal Data Attempted: %0.4f MB"
+                              % iteration_write_size)
+                        print("\t\tTotal Time to write: %0.4f secs"
+                              % iteration_write_time)
+                        print("\t\tAverage Write Time: %0.4f secs" %
+                              (iteration_write_time / args.iterations))
+                        try:
+                            avg_write_speed = (iteration_write_size /
+                                               iteration_write_time)
+                        except ZeroDivisionError:
+                            avg_write_speed = 0.00
+                        finally:
+                            print("\t\tAverage Write Speed: %0.4f MB/s" %
+                                  avg_write_speed)
+                finally:
+                    for key in range(args.count):
+                        test.clean_up(test_files[key].tfile.name)
+                    if (len(test.rem_disks_nm) > 0):
+                        if test.umount() != 0:
+                            errors += 1
+                        test.clean_tmp_dir()
+                    dmesg = subprocess.run(['dmesg'], stdout=subprocess.PIPE)
+                    if 'I/O error' in dmesg.stdout.decode():
+                        logging.error("I/O errors found in dmesg")
+                        errors += 1
+
+                if errors > 0:
+                    logging.warning(
+                        "Completed %s test iterations, but there were"
+                        " errors", args.count)
+                    return 1
+                else:
+                    # LP: 1313581
+                    # Try to figure out whether the disk
+                    # is SuperSpeed USB and using xhci_hcd driver.
+                    if (args.driver == 'xhci_hcd'):
+                        # The speed reported by udisks is sometimes
+                        # less than 5G bits/s, for example,
+                        # it may be 705032705 bits/s
+                        # So using
+                        # 500000000
+                        # = 500 M bits/s
+                        # > 480 M bits/s ( USB 2.0 spec.)
+                        # to make sure that it is higher USB version than 2.0
+                        #
+                        # int() for int(test.rem_disks_speed[disk])
+                        # is necessary
+                        # because the speed value of
+                        # the dictionary rem_disks_speed is
+                        # 1. str or int from _probe_disks_udisks2
+                        # 2. int from _probe_disks_udisks1.
+                        # This is really a mess. : (
+                        print("\t\t--------------------------------")
+                        if(500000000 < int(test.rem_disks_speed[disk])):
+                            print("\t\tDevice Detected: SuperSpeed USB")
+                            # Unlike rem_disks_speed,
+                            # which must has the connect speed
+                            # for each disk devices,
+                            # disk devices may not use xhci as
+                            # controller drivers.
+                            # This will raise KeyError for no
+                            # associated disk device was found.
+                            if test.get_disks_xhci().get(disk, '') != 'xhci':
+                                raise SystemExit(
+                                    "\t\tDisk does not use xhci_hcd.")
+                            print("\t\tDriver Detected: xhci_hcd")
+                        else:
+                            # Give it a hint for the detection failure.
+                            # LP: #1362902
+                            print(("\t\tNo SuperSpeed USB using xhci_hcd "
+                                   "was detected correctly."))
+                            print(("\t\tHint: please use dmesg to check "
+                                   "the system status again."))
+                            return 1
+                    # Pass is not assured
+                    if (not args.pass_speed or
+                            avg_write_speed >= args.pass_speed):
+                        return 0
+                    else:
+                        print("FAIL: Average speed was lower than desired "
+                              "pass speed of %s MB/s" % args.pass_speed)
+                        return 1
+            else:
+                logging.error("No device being mounted successfully "
+                              "for testing, aborting")
+                return 1
+
+    else:  # If we don't have removable drives attached and mounted
+        logging.error("No removable drives were detected, aborting")
+        return 1
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/removable_storage_watcher.py b/org.oniroproject.integration-tests/bin/removable_storage_watcher.py
new file mode 100755
index 0000000000000000000000000000000000000000..988abc3f5b2fab4e6573a5fea717cbef0e281deb
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/removable_storage_watcher.py
@@ -0,0 +1,958 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import copy
+import dbus
+import logging
+import os
+import sys
+import threading
+
+import gi
+gi.require_version('GUdev', '1.0')
+from gi.repository import GObject, GUdev  # noqa: E402
+
+from checkbox_support.dbus import connect_to_system_bus         # noqa: E402
+from checkbox_support.dbus.udisks2 import UDisks2Model          # noqa: E402
+from checkbox_support.dbus.udisks2 import UDisks2Observer       # noqa: E402
+from checkbox_support.dbus.udisks2 import is_udisks2_supported  # noqa: E402
+from checkbox_support.dbus.udisks2 import lookup_udev_device    # noqa: E402
+from checkbox_support.dbus.udisks2 import (                     # noqa: E402
+    map_udisks1_connection_bus)  # noqa: E402
+from checkbox_support.heuristics.udisks2 import is_memory_card  # noqa: E402
+from checkbox_support.parsers.udevadm import CARD_READER_RE     # noqa: E402
+from checkbox_support.parsers.udevadm import GENERIC_RE         # noqa: E402
+from checkbox_support.parsers.udevadm import FLASH_RE           # noqa: E402
+from checkbox_support.scripts.zapper_proxy import (             # noqa: E402
+    ControlVersionDecider)
+from checkbox_support.udev import get_interconnect_speed        # noqa: E402
+from checkbox_support.udev import get_udev_block_devices        # noqa: E402
+
+# Record representing properties of a UDisks1 Drive object needed by the
+# UDisks1 version of the watcher implementation
+UDisks1DriveProperties = collections.namedtuple(
+    'UDisks1DriveProperties', 'file bus speed model vendor media')
+
+# Delta record that encapsulates difference:
+# delta_dir -- directon of the difference, either DELTA_DIR_PLUS or
+#              DELTA_DIR_MINUS
+# value -- the actual value being removed or added, either InterfaceDelta or
+# PropertyDelta instance, see below
+DeltaRecord = collections.namedtuple("DeltaRecord", "delta_dir value")
+
+# Delta value for representing interface changes
+InterfaceDelta = collections.namedtuple(
+    "InterfaceDelta",
+    "delta_type object_path iface_name")
+
+# Delta value for representing property changes
+PropertyDelta = collections.namedtuple(
+    "PropertyDelta",
+    "delta_type object_path iface_name prop_name prop_value")
+
+# Tokens that encode additions and removals
+DELTA_DIR_PLUS = '+'
+DELTA_DIR_MINUS = '-'
+
+# Tokens that encode interface and property deltas
+DELTA_TYPE_IFACE = 'i'
+DELTA_TYPE_PROP = 'p'
+
+
+def format_bytes(size):
+    """
+    Format size to be easily read by humans
+
+    The result is disk-size compatible (using multiples of 10
+    rather than 2) string like "4.5GB"
+    """
+    for index, prefix in enumerate(" KMGTPEZY", 0):
+        factor = 10 ** (index * 3)
+        if size // factor <= 1000:
+            break
+    return "{}{}B".format(size // factor, prefix.strip())
+
+
+class UDisks1StorageDeviceListener:
+
+    def __init__(self, system_bus, loop, action, devices, minimum_speed,
+                 memorycard):
+        self._action = action
+        self._devices = devices
+        self._minimum_speed = minimum_speed
+        self._memorycard = memorycard
+        self._bus = system_bus
+        self._loop = loop
+        self._error = False
+        self._change_cache = []
+
+    def check(self, timeout):
+        udisks = 'org.freedesktop.UDisks'
+        if self._action == 'insert':
+            signal = 'DeviceAdded'
+            logging.debug("Adding signal listener for %s.%s", udisks, signal)
+            self._bus.add_signal_receiver(self.add_detected,
+                                          signal_name=signal,
+                                          dbus_interface=udisks)
+        elif self._action == 'remove':
+            signal = 'DeviceRemoved'
+            logging.debug("Adding signal listener for %s.%s", udisks, signal)
+            self._bus.add_signal_receiver(self.remove_detected,
+                                          signal_name=signal,
+                                          dbus_interface=udisks)
+
+        self._starting_devices = self.get_existing_devices()
+        logging.debug("Starting with the following devices: %r",
+                      self._starting_devices)
+
+        def timeout_callback():
+            print("%s seconds have expired "
+                  "waiting for the device to be inserted." % timeout)
+            self._error = True
+            self._loop.quit()
+
+        logging.debug("Adding timeout listener, timeout=%r", timeout)
+        GObject.timeout_add_seconds(timeout, timeout_callback)
+        logging.debug("Starting event loop...")
+        self._loop.run()
+
+        return self._error
+
+    def verify_device_change(self, changed_devices, message=""):
+        logging.debug("Verifying device change: %s", changed_devices)
+        # Filter the applicable bus types, as provided on the command line
+        # (values of self._devices can be 'usb', 'firewire', etc)
+        desired_bus_devices = [
+            device
+            for device in changed_devices
+            if device.bus in self._devices]
+        logging.debug("Desired bus devices: %s", desired_bus_devices)
+        for dev in desired_bus_devices:
+            if self._memorycard:
+                if (
+                    dev.bus != 'sdio' and
+                    not FLASH_RE.search(dev.media) and
+                    not CARD_READER_RE.search(dev.model) and
+                    not GENERIC_RE.search(dev.vendor)
+                ):
+                    logging.debug(
+                        "The device does not seem to be a memory"
+                        " card (bus: %r, model: %r), skipping",
+                        dev.bus, dev.model)
+                    return
+                print(message % {'bus': 'memory card', 'file': dev.file})
+            else:
+                if (
+                    FLASH_RE.search(dev.media) or
+                    CARD_READER_RE.search(dev.model) or
+                    GENERIC_RE.search(dev.vendor)
+                ):
+                    logging.debug("The device seems to be a memory"
+                                  " card (bus: %r (model: %r), skipping",
+                                  dev.bus, dev.model)
+                    return
+                print(message % {'bus': dev.bus, 'file': dev.file})
+            if self._minimum_speed:
+                if dev.speed >= self._minimum_speed:
+                    print("with speed of %(speed)s bits/s "
+                          "higher than %(min_speed)s bits/s" %
+                          {'speed': dev.speed,
+                           'min_speed': self._minimum_speed})
+                else:
+                    print("ERROR: speed of %(speed)s bits/s lower "
+                          "than %(min_speed)s bits/s" %
+                          {'speed': dev.speed,
+                           'min_speed': self._minimum_speed})
+                    self._error = True
+            logging.debug("Device matches requirements, exiting event loop")
+            self._loop.quit()
+
+    def job_change_detected(self, devices, job_in_progress, job_id,
+                            job_num_tasks, job_cur_task_id,
+                            job_cur_task_percentage):
+        logging.debug("UDisks1 reports a job change has been detected:"
+                      " devices: %s, job_in_progress: %s, job_id: %s,"
+                      " job_num_tasks: %s, job_cur_task_id: %s,"
+                      " job_cur_task_percentage: %s",
+                      devices, job_in_progress, job_id, job_num_tasks,
+                      job_cur_task_id, job_cur_task_percentage)
+        if job_id == "FilesystemMount":
+            if devices in self._change_cache:
+                logging.debug("Ignoring filesystem mount,"
+                              " the device is present in change cache")
+                return
+            logging.debug("Adding devices to change cache: %r", devices)
+            self._change_cache.append(devices)
+            logging.debug("Starting devices were: %s", self._starting_devices)
+            current_devices = self.get_existing_devices()
+            logging.debug("Current devices are: %s", current_devices)
+            inserted_devices = list(set(current_devices) -
+                                    set(self._starting_devices))
+            logging.debug("Computed inserted devices: %s", inserted_devices)
+            if self._memorycard:
+                message = "Expected memory card device %(file)s inserted"
+            else:
+                message = "Expected %(bus)s device %(file)s inserted"
+            self.verify_device_change(inserted_devices, message=message)
+
+    def add_detected(self, added_path):
+        logging.debug("UDisks1 reports device has been added: %s", added_path)
+        logging.debug("Resetting change_cache to []")
+        self._change_cache = []
+        signal_name = 'DeviceJobChanged'
+        dbus_interface = 'org.freedesktop.UDisks'
+        logging.debug("Adding signal listener for %s.%s",
+                      dbus_interface, signal_name)
+        self._bus.add_signal_receiver(self.job_change_detected,
+                                      signal_name=signal_name,
+                                      dbus_interface=dbus_interface)
+
+    def remove_detected(self, removed_path):
+        logging.debug("UDisks1 reports device has been removed: %s",
+                      removed_path)
+
+        logging.debug("Starting devices were: %s", self._starting_devices)
+        current_devices = self.get_existing_devices()
+        logging.debug("Current devices are: %s", current_devices)
+        removed_devices = list(set(self._starting_devices) -
+                               set(current_devices))
+        logging.debug("Computed removed devices: %s", removed_devices)
+        self.verify_device_change(
+            removed_devices,
+            message="Removable %(bus)s device %(file)s has been removed")
+
+    def get_existing_devices(self):
+        logging.debug("Getting existing devices from UDisks1")
+        ud_manager_obj = self._bus.get_object("org.freedesktop.UDisks",
+                                              "/org/freedesktop/UDisks")
+        ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks')
+        existing_devices = []
+        for dev in ud_manager.EnumerateDevices():
+            try:
+                device_obj = self._bus.get_object("org.freedesktop.UDisks",
+                                                  dev)
+                device_props = dbus.Interface(device_obj,
+                                              dbus.PROPERTIES_IFACE)
+                udisks = 'org.freedesktop.UDisks.Device'
+                _device_file = device_props.Get(udisks, "DeviceFile")
+                _bus = device_props.Get(udisks, "DriveConnectionInterface")
+                _speed = device_props.Get(udisks, "DriveConnectionSpeed")
+                _parent_model = ''
+                _parent_media = ''
+                _parent_vendor = ''
+
+                if device_props.Get(udisks, "DeviceIsPartition"):
+                    parent_obj = self._bus.get_object(
+                        "org.freedesktop.UDisks",
+                        device_props.Get(udisks, "PartitionSlave"))
+                    parent_props = dbus.Interface(
+                        parent_obj, dbus.PROPERTIES_IFACE)
+                    _parent_model = parent_props.Get(udisks, "DriveModel")
+                    _parent_vendor = parent_props.Get(udisks, "DriveVendor")
+                    _parent_media = parent_props.Get(udisks, "DriveMedia")
+
+                if not device_props.Get(udisks, "DeviceIsDrive"):
+                    device = UDisks1DriveProperties(
+                        file=str(_device_file),
+                        bus=str(_bus),
+                        speed=int(_speed),
+                        model=str(_parent_model),
+                        vendor=str(_parent_vendor),
+                        media=str(_parent_media))
+                    existing_devices.append(device)
+
+            except dbus.DBusException:
+                pass
+
+        return existing_devices
+
+
+def udisks2_objects_delta(old, new):
+    """
+    Compute the delta between two snapshots of udisks2 objects
+
+    The objects are encoded as {s:{s:{s:v}}} where the first dictionary maps
+    from DBus object path to a dictionary that maps from interface name to a
+    dictionary that finally maps from property name to property value.
+
+    The result is a generator of DeltaRecord objects that encodes the changes:
+        * the 'delta_dir' is either DELTA_DIR_PLUS or DELTA_DIR_MINUS
+        * the 'value' is a tuple that differs for interfaces and properties.
+          Interfaces use the format (DELTA_TYPE_IFACE, object_path, iface_name)
+          while properties use the format (DELTA_TYPE_PROP, object_path,
+          iface_name, prop_name, prop_value)
+
+    Interfaces are never "changed", they are only added or removed. Properties
+    can be changed and this is encoded as removal followed by an addition where
+    both differ only by the 'delta_dir' and the last element of the 'value'
+    tuple.
+    """
+    # Traverse all objects, old or new
+    all_object_paths = set()
+    all_object_paths |= old.keys()
+    all_object_paths |= new.keys()
+    for object_path in sorted(all_object_paths):
+        old_object = old.get(object_path, {})
+        new_object = new.get(object_path, {})
+        # Traverse all interfaces of each object, old or new
+        all_iface_names = set()
+        all_iface_names |= old_object.keys()
+        all_iface_names |= new_object.keys()
+        for iface_name in sorted(all_iface_names):
+            if iface_name not in old_object and iface_name in new_object:
+                # Report each ADDED interface
+                assert iface_name in new_object
+                delta_value = InterfaceDelta(
+                    DELTA_TYPE_IFACE, object_path, iface_name)
+                yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
+                # Report all properties ADDED on that interface
+                for prop_name, prop_value in new_object[iface_name].items():
+                    delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path,
+                                                iface_name, prop_name,
+                                                prop_value)
+                    yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
+            elif iface_name not in new_object and iface_name in old_object:
+                # Report each REMOVED interface
+                assert iface_name in old_object
+                delta_value = InterfaceDelta(
+                    DELTA_TYPE_IFACE, object_path, iface_name)
+                yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
+                # Report all properties REMOVED on that interface
+                for prop_name, prop_value in old_object[iface_name].items():
+                    delta_value = PropertyDelta(DELTA_TYPE_PROP, object_path,
+                                                iface_name, prop_name,
+                                                prop_value)
+                    yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
+            else:
+                # Analyze properties of each interface that existed both in old
+                # and new object trees.
+                assert iface_name in new_object
+                assert iface_name in old_object
+                old_props = old_object[iface_name]
+                new_props = new_object[iface_name]
+                all_prop_names = set()
+                all_prop_names |= old_props.keys()
+                all_prop_names |= new_props.keys()
+                # Traverse all properties, old or new
+                for prop_name in sorted(all_prop_names):
+                    if prop_name not in old_props and prop_name in new_props:
+                        # Report each ADDED property
+                        delta_value = PropertyDelta(
+                            DELTA_TYPE_PROP, object_path, iface_name,
+                            prop_name, new_props[prop_name])
+                        yield DeltaRecord(DELTA_DIR_PLUS, delta_value)
+                    elif prop_name not in new_props and prop_name in old_props:
+                        # Report each REMOVED property
+                        delta_value = PropertyDelta(
+                            DELTA_TYPE_PROP, object_path, iface_name,
+                            prop_name, old_props[prop_name])
+                        yield DeltaRecord(DELTA_DIR_MINUS, delta_value)
+                    else:
+                        old_value = old_props[prop_name]
+                        new_value = new_props[prop_name]
+                        if old_value != new_value:
+                            # Report each changed property
+                            yield DeltaRecord(DELTA_DIR_MINUS, PropertyDelta(
+                                DELTA_TYPE_PROP, object_path, iface_name,
+                                prop_name, old_value))
+                            yield DeltaRecord(DELTA_DIR_PLUS, PropertyDelta(
+                                DELTA_TYPE_PROP, object_path, iface_name,
+                                prop_name, new_value))
+
+
+class UDisks2StorageDeviceListener:
+    """
+    Implementation of the storage device listener concept for UDisks2 backend.
+    Loosely modeled on the UDisks-based implementation above.
+
+    Implementation details
+    ^^^^^^^^^^^^^^^^^^^^^^
+
+    The class, once configured reacts to asynchronous events from the event
+    loop. Those are either DBus signals or GLib timeout.
+
+    The timeout, if reached, terminates the test and fails with an appropriate
+    end-user message. The user is expected to manipulate storage devices while
+    the test is running.
+
+    DBus signals (that correspond to UDisks2 DBus signals) cause callbacks into
+    this code. Each time a signal is reported "delta" is computed and verified
+    to determine if there was a successful match. The delta contains a list or
+    DeltaRecord objects that encode difference (either addition or removal) and
+    the value of the difference (interface name or interface property value).
+    This delta is computed by udisks2_objects_delta(). The delta is then passed
+    to _validate_delta() which has a chance to end the test but also prints
+    diagnostic messages in verbose mode. This is very useful for understanding
+    what the test actually sees occurring.
+
+    Insertion/removal detection strategy
+    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+    Compared to initial state, the following changes objects need to be
+    detected
+
+    * At least one UDisks2 object with the following _all_ interfaces:
+        * UDisks2.Partition
+            (because we want a partitioned device)
+        * UDisks2.Block
+            (because we want that device to have a block device that users can
+            format)
+            - having IdUsage == 'filesystem'
+            (because it should not be a piece of raid or lvm)
+            - having Size > 0
+            (because it should not be and empty removable storage reader)
+        * UDisks2.Filesystem
+            (because we want to ensure that a filesystem gets mounted)
+            - having MountPoints != []
+            - as a special exception this rule is REMOVED from eSATA and SATA
+              devices as they are not automatically mounted anymore.
+
+    This object must be traceable to an UDisks.Drive object:
+        (because we need the medium to be inserted somewhere)
+            - having ConnectionBus in (desired_connection_buses)
+            - as a special exception this rule is weakened for eSATA because
+              for such devices the ConnectionBus property is empty.
+    """
+
+    # Name of the DBus interface exposed UDisks2 for various drives
+    UDISKS2_DRIVE_INTERFACE = "org.freedesktop.UDisks2.Drive"
+
+    # Name of the DBus property provided by the "Drive" interface above
+    UDISKS2_DRIVE_PROPERTY_CONNECTION_BUS = "ConnectionBus"
+
+    def __init__(self, system_bus, loop, action, devices, minimum_speed,
+                 memorycard, unmounted=False):
+        # Store the desired minimum speed of the device in Mbit/s. The argument
+        # is passed as the number of bits per second so let's fix that.
+        self._desired_minimum_speed = minimum_speed / 10 ** 6
+        # Compute the allowed UDisks2.Drive.ConnectionBus value based on the
+        # legacy arguments passed from the command line.
+        self._desired_connection_buses = set([
+            map_udisks1_connection_bus(device) for device in devices])
+        # Check if we are explicitly looking for memory cards
+        self._desired_memory_card = memorycard
+        # Store information whether we also want detected, but unmounted
+        # devices too
+        self._allow_unmounted = unmounted
+        # Store the desired "delta" direction depending on
+        # whether we test for insertion or removal
+        if action == "insert":
+            self._desired_delta_dir = DELTA_DIR_PLUS
+        elif action == "remove":
+            self._desired_delta_dir = DELTA_DIR_MINUS
+        else:
+            raise ValueError("Unsupported action: {}".format(action))
+        # Store DBus bus object as we need to pass it to UDisks2 observer
+        self._bus = system_bus
+        # Store event loop object
+        self._loop = loop
+        # Setup UDisks2Observer class to track changes published by UDisks2
+        self._udisks2_observer = UDisks2Observer()
+        # Set the initial value of reference_objects.
+        # The actual value is only set once in check()
+        self._reference_objects = None
+        # As above, just initializing in init for sake of consistency
+        self._is_reference = None
+        # Setup UDisks2Model to know what the current state is. This is needed
+        # when remove events are reported as they don't carry enough state for
+        # the program to work correctly. Since UDisks2Model only applies the
+        # changes _after_ processing the signals from UDisks2Observer we can
+        # reliably check all of the properties of the removed object / device.
+        self._udisks2_model = UDisks2Model(self._udisks2_observer)
+        # Whenever anything changes call our local change handler
+        # This handler always computes the full delta (versus the
+        # reference state) and decides if we have a match or not
+        self._udisks2_model.on_change.connect(self._on_change)
+        # We may need an udev context for checking the speed of USB devices
+        self._udev_client = GUdev.Client()
+        # A snapshot of udev devices, set in check()
+        self._reference_udev_devices = None
+        # Assume the test passes, this is changed when timeout expires or when
+        # an incorrect device gets inserted.
+        self._error = False
+
+    def _dump_reference_udisks_objects(self):
+        logging.debug("Reference UDisks2 objects:")
+        for udisks2_object in self._reference_objects:
+            logging.debug(" - %s", udisks2_object)
+
+    def _dump_reference_udev_devices(self):
+        logging.debug("Reference udev devices:")
+        for udev_device in self._reference_udev_devices:
+            interconnect_speed = get_interconnect_speed(udev_device)
+            if interconnect_speed:
+                logging.debug(" - %s (USB %dMBit/s)",
+                              udev_device.get_device_file(),
+                              interconnect_speed)
+            else:
+                logging.debug(" - %s", udev_device.get_device_file())
+
+    def check(self, timeout):
+        """
+        Run the configured test and return the result
+
+        The result is False if the test has failed.  The timeout, when
+        non-zero, will make the test fail after the specified seconds have
+        elapsed without conclusive result.
+        """
+        # Setup a timeout if requested
+        if timeout > 0:
+            GObject.timeout_add_seconds(timeout, self._on_timeout_expired)
+        # Connect the observer to the bus. This will start giving us events
+        # (actually when the loop starts later below)
+        self._udisks2_observer.connect_to_bus(self._bus)
+        # Get the reference snapshot of available devices
+        self._reference_objects = copy.deepcopy(self._current_objects)
+        self._dump_reference_udisks_objects()
+        # Mark the current _reference_objects as ... reference, this is sadly
+        # needed by _summarize_changes() as it sees the snapshot _after_ a
+        # change has occurred and cannot determine if the slope of the 'edge'
+        # of the change. It is purely needed for UI in verbose mode
+        self._is_reference = True
+        # A collection of objects that we gladly ignore because we already
+        # reported on them being somehow inappropriate
+        self._ignored_objects = set()
+        # Get the reference snapshot of available udev devices
+        self._reference_udev_devices = get_udev_block_devices(
+            self._udev_client)
+        self._dump_reference_udev_devices()
+        # Start the loop and wait. The loop will exit either when:
+        # 1) A proper device has been detected (either insertion or removal)
+        # 2) A timeout (optional) has expired
+        self._loop.run()
+        # Return the outcome of the test
+        return self._error
+
+    def _on_timeout_expired(self):
+        """
+        Internal function called when the timer expires.
+
+        Basically it's just here to tell the user the test failed or that the
+        user was unable to alter the device during the allowed time.
+        """
+        print("You have failed to perform the required manipulation in time")
+        # Fail the test when the timeout was reached
+        self._error = True
+        # Stop the loop now
+        self._loop.quit()
+
+    def _on_change(self):
+        """
+        Internal method called by UDisks2Model whenever a change had occurred
+        """
+        # Compute the changes that had occurred since the reference point
+        delta_records = list(self._get_delta_records())
+        # Display a summary of changes when we are done
+        self._summarize_changes(delta_records)
+        # If the changes are what we wanted stop the loop
+        matching_devices = self._get_matching_devices(delta_records)
+        if matching_devices:
+            print("Expected device manipulation complete: {}".format(
+                ', '.join(matching_devices)))
+            # And call it a day
+            self._loop.quit()
+
+    def _get_matching_devices(self, delta_records):
+        """
+        Internal method called that checks if the delta records match the type
+        of device manipulation we were expecting. Only called from _on_change()
+
+        Returns a set of paths of block devices that matched
+        """
+        # Results
+        results = set()
+        # Group changes by DBus object path
+        grouped_records = collections.defaultdict(list)
+        for record in delta_records:
+            grouped_records[record.value.object_path].append(record)
+        # Create another snapshot od udev devices so that we don't do it over
+        # and over in the loop below (besides, if we did that then results
+        # could differ each time).
+        current_udev_devices = get_udev_block_devices(self._udev_client)
+        # Iterate over all UDisks2 objects and their delta records
+        for object_path, records_for_object in grouped_records.items():
+            # Skip objects we already ignored and complained about before
+            if object_path in self._ignored_objects:
+                continue
+            needs = set(('block-fs', 'partition', 'non-empty'))
+            if not self._allow_unmounted:
+                needs.add('mounted')
+
+            # As a special exception when the ConnectionBus is allowed to be
+            # empty, as is the case with eSATA devices, do not require the
+            # filesystem to be mounted as gvfs may choose not to mount it
+            # automatically.
+            found = set()
+            drive_object_path = None
+            object_block_device = None
+            for record in records_for_object:
+                # Skip changes opposite to the ones we need
+                if record.delta_dir != self._desired_delta_dir:
+                    continue
+                # For devices with empty "ConnectionBus" property, don't
+                # require the device to be mounted
+                if (
+                    record.value.iface_name ==
+                    "org.freedesktop.UDisks2.Drive" and
+                    record.value.delta_type == DELTA_TYPE_PROP and
+                    record.value.prop_name == "ConnectionBus" and
+                    record.value.prop_value == ""
+                ):
+                    needs.remove('mounted')
+                # Detect block devices designated for filesystems
+                if (
+                    record.value.iface_name ==
+                    "org.freedesktop.UDisks2.Block" and
+                    record.value.delta_type == DELTA_TYPE_PROP and
+                    record.value.prop_name == "IdUsage" and
+                    record.value.prop_value == "filesystem"
+                ):
+                    found.add('block-fs')
+                # Memorize the block device path
+                elif (
+                    record.value.iface_name ==
+                    "org.freedesktop.UDisks2.Block" and
+                    record.value.delta_type == DELTA_TYPE_PROP and
+                    record.value.prop_name == "PreferredDevice"
+                ):
+                    object_block_device = record.value.prop_value
+                # Ensure the device is a partition
+                elif (record.value.iface_name ==
+                      "org.freedesktop.UDisks2.Partition" and
+                      record.value.delta_type == DELTA_TYPE_IFACE):
+                    found.add('partition')
+                # Ensure the device is not empty
+                elif (record.value.iface_name ==
+                      "org.freedesktop.UDisks2.Block" and
+                      record.value.delta_type == DELTA_TYPE_PROP and
+                      record.value.prop_name == "Size" and
+                      record.value.prop_value > 0):
+                    found.add('non-empty')
+                # Ensure the filesystem is mounted
+                elif (record.value.iface_name ==
+                      "org.freedesktop.UDisks2.Filesystem" and
+                      record.value.delta_type == DELTA_TYPE_PROP and
+                      record.value.prop_name == "MountPoints" and
+                      record.value.prop_value != []):
+                    found.add('mounted')
+                    # On some systems partition are reported as mounted
+                    # filesystems, without 'partition' record
+                    if set(['partition', 'mounted']).issubset(needs):
+                        needs.remove('partition')
+                # Finally memorize the drive the block device belongs to
+                elif (record.value.iface_name ==
+                      "org.freedesktop.UDisks2.Block" and
+                      record.value.delta_type == DELTA_TYPE_PROP and
+                      record.value.prop_name == "Drive"):
+                    drive_object_path = record.value.prop_value
+            logging.debug("Finished analyzing %s, found: %s, needs: %s"
+                          " drive_object_path: %s", object_path, found, needs,
+                          drive_object_path)
+            if not needs.issubset(found) or drive_object_path is None:
+                continue
+            # We've found our candidate, let's look at the drive it belongs
+            # to. We need to do this as some properties are associated with
+            # the drive, not the filesystem/block device and the drive may
+            # not have been inserted at all.
+            try:
+                drive_object = self._current_objects[drive_object_path]
+            except KeyError:
+                # The drive may be removed along with the device, let's check
+                # if we originally saw it
+                try:
+                    drive_object = self._reference_objects[drive_object_path]
+                except KeyError:
+                    logging.error(
+                        "A block device belongs to a drive we could not find")
+                    logging.error("missing drive: %r", drive_object_path)
+                    continue
+            try:
+                drive_props = drive_object["org.freedesktop.UDisks2.Drive"]
+            except KeyError:
+                logging.error(
+                    "A block device belongs to an object that is not a Drive")
+                logging.error("strange object: %r", drive_object_path)
+                continue
+            # Ensure the drive is on the appropriate bus
+            connection_bus = drive_props["ConnectionBus"]
+            if connection_bus not in self._desired_connection_buses:
+                logging.warning("The object %r belongs to drive %r that"
+                                " is attached to the bus %r but but we are"
+                                " looking for one of %r so it cannot match",
+                                object_block_device, drive_object_path,
+                                connection_bus,
+                                ", ".join(self._desired_connection_buses))
+                # Ignore this object so that we don't spam the user twice
+                self._ignored_objects.add(object_path)
+                continue
+            # Ensure it is a media card reader if this was explicitly requested
+            drive_is_reader = is_memory_card(
+                drive_props['Vendor'], drive_props['Model'],
+                drive_props['Media'])
+            if self._desired_memory_card and not drive_is_reader:
+                logging.warning(
+                    "The object %s belongs to drive %s that does not seem to"
+                    " be a media reader", object_block_device,
+                    drive_object_path)
+                # Ignore this object so that we don't spam the user twice
+                self._ignored_objects.add(object_path)
+                continue
+            # Ensure the desired minimum speed is enforced
+            if self._desired_minimum_speed:
+                # We need to discover the speed of the UDisks2 object that is
+                # about to be matched. Sadly UDisks2 no longer supports this
+                # property so we need to poke deeper and resort to udev.
+                #
+                # The UDisks2 object that we are interested in implements a
+                # number of interfaces, most notably
+                # org.freedesktop.UDisks2.Block, that has the Device property
+                # holding the unix filesystem path (like /dev/sdb1). We already
+                # hold a reference to that as 'object_block_device'
+                #
+                # We take this as a start and attempt to locate the udev Device
+                # (don't confuse with UDisks2.Device, they are _not_ the same)
+                # that is associated with that path.
+                if self._desired_delta_dir == DELTA_DIR_PLUS:
+                    # If we are looking for additions then look at _current_
+                    # collection of udev devices
+                    udev_devices = current_udev_devices
+                    udisks2_object = self._current_objects[object_path]
+                else:
+                    # If we are looking for removals then look at referece
+                    # collection of udev devices
+                    udev_devices = self._reference_udev_devices
+                    udisks2_object = self._reference_objects[object_path]
+                try:
+                    # Try to locate the corresponding udev device among the
+                    # collection we've selected. Use the drive object as the
+                    # key -- this looks for the drive, not partition objects!
+                    udev_device = lookup_udev_device(udisks2_object,
+                                                     udev_devices)
+                except LookupError:
+                    logging.error("Unable to map UDisks2 object %s to udev",
+                                  object_block_device)
+                    # Ignore this object so that we don't spam the user twice
+                    self._ignored_objects.add(object_path)
+                    continue
+                interconnect_speed = get_interconnect_speed(udev_device)
+                # Now that we know the speed of the interconnect we can try to
+                # validate it against our desired speed.
+                if interconnect_speed is None:
+                    logging.warning("Unable to determine interconnect speed of"
+                                    " device %s", object_block_device)
+                    # Ignore this object so that we don't spam the user twice
+                    self._ignored_objects.add(object_path)
+                    continue
+                elif interconnect_speed < self._desired_minimum_speed:
+                    logging.warning(
+                        "Device %s is connected via an interconnect that has"
+                        " the speed of %dMbit/s but the required speed was"
+                        " %dMbit/s", object_block_device, interconnect_speed,
+                        self._desired_minimum_speed)
+                    # Ignore this object so that we don't spam the user twice
+                    self._ignored_objects.add(object_path)
+                    continue
+                else:
+                    logging.info("Device %s is connected via an USB"
+                                 " interconnect with the speed of %dMbit/s",
+                                 object_block_device, interconnect_speed)
+            # Yay, success
+            results.add(object_block_device)
+        return results
+
+    @property
+    def _current_objects(self):
+        return self._udisks2_model.managed_objects
+
+    def _get_delta_records(self):
+        """
+        Internal method used to compute the delta between reference devices and
+        current devices. The result is a generator of DeltaRecord objects.
+        """
+        assert self._reference_objects is not None, "Only usable after check()"
+        old = self._reference_objects
+        new = self._current_objects
+        return udisks2_objects_delta(old, new)
+
+    def _summarize_changes(self, delta_records):
+        """
+        Internal method used to summarize changes (compared to reference state)
+        called whenever _on_change() gets called. Only visible in verbose mode
+        """
+        # Filter out anything but interface changes
+        flat_records = [record
+                        for record in delta_records
+                        if record.value.delta_type == DELTA_TYPE_IFACE]
+        # Group changes by DBus object path
+        grouped_records = collections.defaultdict(list)
+        for record in flat_records:
+            grouped_records[record.value.object_path].append(record)
+        # Bail out quickly when nothing got changed
+        if not flat_records:
+            if not self._is_reference:
+                logging.info("You have returned to the reference state")
+                self._is_reference = True
+            return
+        else:
+            self._is_reference = False
+        # Iterate over grouped delta records for all objects
+        logging.info("Compared to the reference state you have:")
+        for object_path in sorted(grouped_records.keys()):
+            records_for_object = sorted(
+                grouped_records[object_path],
+                key=lambda record: record.value.iface_name)
+            # Skip any job objects as they just add noise
+            if any((record.value.iface_name == "org.freedesktop.UDisks2.Job"
+                    for record in records_for_object)):
+                continue
+            logging.info("For object %s", object_path)
+            for record in records_for_object:
+                # Ignore property changes for now
+                if record.value.delta_type != DELTA_TYPE_IFACE:
+                    continue
+                # Get the name of the interface that was affected
+                iface_name = record.value.iface_name
+                # Get the properties for that interface (for removals get the
+                # reference values, for additions get the current values)
+                if record.delta_dir == DELTA_DIR_PLUS:
+                    props = self._current_objects[object_path][iface_name]
+                    action = "inserted"
+                else:
+                    props = self._reference_objects[object_path][iface_name]
+                    action = "removed"
+                # Display some human-readable information associated with each
+                # interface change
+                if iface_name == "org.freedesktop.UDisks2.Drive":
+                    logging.info("\t * %s a drive", action)
+                    logging.info("\t   vendor and name: %r %r",
+                                 props['Vendor'], props['Model'])
+                    logging.info("\t   bus: %s", props['ConnectionBus'])
+                    logging.info("\t   size: %s", format_bytes(props['Size']))
+                    logging.info("\t   is media card: %s", is_memory_card(
+                        props['Vendor'], props['Model'], props['Media']))
+                    logging.info("\t   current media: %s",
+                                 props['Media'] or "???" if
+                                 props['MediaAvailable'] else "N/A")
+                elif iface_name == "org.freedesktop.UDisks2.Block":
+                    logging.info("\t * %s block device", action)
+                    logging.info("\t   from drive: %s", props['Drive'])
+                    logging.info("\t   having device: %s", props['Device'])
+                    logging.info("\t   having usage, type and version:"
+                                 " %s %s %s", props['IdUsage'],
+                                 props['IdType'], props['IdVersion'])
+                    logging.info("\t   having label: %s", props['IdLabel'])
+                elif iface_name == "org.freedesktop.UDisks2.PartitionTable":
+                    logging.info("\t * %s partition table", action)
+                    logging.info("\t   having type: %r", props['Type'])
+                elif iface_name == "org.freedesktop.UDisks2.Partition":
+                    logging.info("\t * %s partition", action)
+                    logging.info("\t   from partition table: %s",
+                                 props['Table'])
+                    logging.info("\t   having size: %s",
+                                 format_bytes(props['Size']))
+                    logging.info("\t   having name: %r", props['Name'])
+                elif iface_name == "org.freedesktop.UDisks2.Filesystem":
+                    logging.info("\t * %s file system", action)
+                    logging.info("\t   having mount points: %r",
+                                 props['MountPoints'])
+
+
+def main():
+    description = "Wait for the specified device to be inserted or removed."
+    parser = argparse.ArgumentParser(description=description)
+    parser.add_argument('action', choices=['insert', 'remove'])
+    parser.add_argument(
+        'device', choices=['usb', 'sdio', 'firewire', 'scsi',
+                           'ata_serial_esata'], nargs="+")
+    memorycard_help = ("Memory cards devices on bus other than sdio require "
+                       "this parameter to identify them as such")
+    parser.add_argument('--memorycard', action="store_true",
+                        help=memorycard_help)
+    parser.add_argument('--timeout', type=int, default=20)
+    min_speed_help = ("Will only accept a device if its connection speed "
+                      "attribute is higher than this value "
+                      "(in bits/s)")
+    parser.add_argument('--minimum_speed', '-m', help=min_speed_help,
+                        type=int, default=0)
+    parser.add_argument('--verbose', action='store_const', const=logging.INFO,
+                        dest='logging_level', help="Enable verbose output")
+    parser.add_argument('--debug', action='store_const', const=logging.DEBUG,
+                        dest='logging_level', help="Enable debugging")
+    parser.add_argument('--unmounted', action='store_true',
+                        help="Don't require drive being automounted")
+    parser.add_argument('--zapper-usb-address', type=str,
+                        help="Zapper's USB switch address to use")
+    parser.set_defaults(logging_level=logging.WARNING)
+    args = parser.parse_args()
+
+    # Configure logging as requested
+    # XXX: This may be incorrect as logging.basicConfig() fails after any other
+    # call to logging.log(). The proper solution is to setup a verbose logging
+    # configuration and I didn't want to do it now.
+    logging.basicConfig(
+        level=args.logging_level,
+        format='[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
+
+    # Connect to the system bus, we also get the event
+    # loop as we need it to start listening for signals.
+    system_bus, loop = connect_to_system_bus()
+
+    # Check if system bus has the UDisks2 object
+    if is_udisks2_supported(system_bus):
+        # Construct the listener with all of the arguments provided on the
+        # command line and the explicit system_bus, loop objects.
+        logging.debug("Using UDisks2 interface")
+        listener = UDisks2StorageDeviceListener(
+            system_bus, loop,
+            args.action, args.device, args.minimum_speed, args.memorycard,
+            args.unmounted)
+    else:
+        # Construct the listener with all of the arguments provided on the
+        # command line and the explicit system_bus, loop objects.
+        logging.debug("Using UDisks1 interface")
+        listener = UDisks1StorageDeviceListener(
+            system_bus, loop,
+            args.action, args.device, args.minimum_speed, args.memorycard)
+    # Run the actual listener and wait till it either times out of discovers
+    # the appropriate media changes
+    if args.zapper_usb_address:
+        zapper_host = os.environ.get('ZAPPER_ADDRESS')
+        if not zapper_host:
+            raise SystemExit(
+                "ZAPPER_ADDRESS environment variable not found!")
+        zapper_control = ControlVersionDecider().decide(zapper_host)
+        usb_address = args.zapper_usb_address
+        delay = 5  # in seconds
+
+        def do_the_insert():
+            logging.info("Calling zapper to connect the USB device")
+            zapper_control.usb_set_state(usb_address, 'dut')
+        insert_timer = threading.Timer(delay, do_the_insert)
+
+        def do_the_remove():
+            logging.info("Calling zapper to disconnect the USB device")
+            zapper_control.usb_set_state(usb_address, 'off')
+        remove_timer = threading.Timer(delay, do_the_remove)
+        if args.action == "insert":
+            logging.info("Starting timer for delayed insertion")
+            insert_timer.start()
+        elif args.action == "remove":
+            logging.info("Starting timer for delayed removal")
+            remove_timer.start()
+        try:
+            res = listener.check(args.timeout)
+            return res
+        except KeyboardInterrupt:
+            return 1
+
+    else:
+        print("\n\n{} NOW\n\n".format(args.action.upper()), flush=True)
+        try:
+            return listener.check(args.timeout)
+        except KeyboardInterrupt:
+            return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/resolution_test.py b/org.oniroproject.integration-tests/bin/resolution_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..77238ff22aa245d4e07e9a44f6fae7efc37ceeee
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/resolution_test.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+
+import gi
+import sys
+
+from argparse import ArgumentParser
+gi.require_version('Gdk', '3.0')
+from gi.repository import Gdk  # noqa: E402
+
+
+def check_resolution():
+    screen = Gdk.Screen.get_default()
+    n = screen.get_n_monitors()
+    for i in range(n):
+        geom = screen.get_monitor_geometry(i)
+        print('Monitor %d:' % (i + 1))
+        print('  %d x %d' % (geom.width, geom.height))
+
+
+def compare_resolution(min_h, min_v):
+    # Evaluate just the primary display
+    screen = Gdk.Screen.get_default()
+    geom = screen.get_monitor_geometry(screen.get_primary_monitor())
+    print("Minimum acceptable display resolution: %d x %d" % (min_h, min_v))
+    print("Detected display resolution: %d x %d" % (geom.width, geom.height))
+    return geom.width >= min_h and geom.height >= min_v
+
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument(
+        "--horizontal",
+        type=int,
+        default=0,
+        help="Minimum acceptable horizontal resolution.")
+    parser.add_argument(
+        "--vertical",
+        type=int,
+        default=0,
+        help="Minimum acceptable vertical resolution.")
+    args = parser.parse_args()
+
+    if (args.horizontal > 0) and (args.vertical > 0):
+        return not compare_resolution(args.horizontal, args.vertical)
+    else:
+        check_resolution()
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/bin/udev_resource.py b/org.oniroproject.integration-tests/bin/udev_resource.py
new file mode 100755
index 0000000000000000000000000000000000000000..4bbc5fe90efe9b33ecda98152059da4dbe7848a1
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/udev_resource.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+#
+# This file is part of Checkbox.
+#
+# Copyright 2011-2016 Canonical Ltd.
+#
+# Checkbox is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3,
+# as published by the Free Software Foundation.
+
+#
+# Checkbox is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
+#
+import argparse
+import shlex
+
+from collections import OrderedDict
+from subprocess import check_output, CalledProcessError
+
+from checkbox_support.parsers.udevadm import UdevadmParser
+
+categories = ("ACCELEROMETER", "AUDIO", "BLUETOOTH", "CAPTURE", "CARDREADER",
+              "CDROM", "DISK", "KEYBOARD", "INFINIBAND", "MMAL", "MOUSE",
+              "NETWORK", "TPU", "OTHER", "PARTITION", "TOUCHPAD",
+              "TOUCHSCREEN", "USB", "VIDEO", "WATCHDOG", "WIRELESS", "WWAN")
+
+attributes = ("path", "name", "bus", "category", "driver", "product_id",
+              "vendor_id", "subproduct_id", "subvendor_id", "product",
+              "vendor", "interface", "mac", "product_slug", "vendor_slug",
+              "symlink_uuid")
+
+
+def dump_udev_db(udev):
+    for device in udev.run():
+        for attribute in attributes:
+            value = getattr(device, attribute)
+            if value is not None:
+                print("%s: %s" % (attribute, value))
+        print()
+
+
+def filter_by_categories(udev, categories):
+    count = 0
+    for device in udev.run():
+        c = getattr(device, "category", None)
+        if c in categories:
+            count += 1
+            for attribute in attributes:
+                value = getattr(device, attribute)
+                if value is not None:
+                    print("%s: %s" % (attribute, value))
+            print()
+    return count
+
+
+def display_by_categories(udev, categories, short=False):
+    count = 0
+    data = OrderedDict()
+    for category in categories:
+        data[category] = []
+    for device in udev.run():
+        c = getattr(device, "category", None)
+        if c in categories:
+            count += 1
+            p = getattr(device, "product", "Unknow product")
+            v = getattr(device, "vendor", "Unknow vendor")
+            vid = device.vendor_id if device.vendor_id else 0
+            pid = device.product_id if device.product_id else 0
+            if not p:
+                p = getattr(device, "interface", "Unknow product")
+            data[c].append(
+                "{} {} [{:04x}:{:04x}]".format(v, p, vid, pid))
+    for c, devices in data.items():
+        if short:
+            for d in devices:
+                print("{}".format(
+                    d.replace('None ', '').replace(' None', '')))
+        else:
+            print("{} ({}):".format(c, len(devices)))
+            for d in devices:
+                print(" - {}".format(d))
+            print()
+    return count
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--command", action='store', type=str,
+                        default="udevadm info --export-db",
+                        help="""Command to execute to get udevadm information.
+                              Only change it if you know what you're doing.""")
+    parser.add_argument("-d", "--lsblkcommand", action='store', type=str,
+                        default="lsblk -i -n -P -o KNAME,TYPE,MOUNTPOINT",
+                        help="""Command to execute to get lsblk information.
+                              Only change it if you know what you're doing.""")
+    parser.add_argument('-l', '--list', nargs='+', choices=categories,
+                        metavar=("CATEGORY"), default=(),
+                        help="""List devices found under the requested
+                        categories.
+                        Acceptable categories to list are:
+                        {}""".format(', '.join(categories)))
+    parser.add_argument('-f', '--filter', nargs='+', choices=categories,
+                        metavar=("CATEGORY"), default=(),
+                        help="""Filter devices found under the requested
+                        categories.
+                        Acceptable categories to list are:
+                        {}""".format(', '.join(categories)))
+    parser.add_argument('-s', '--short', action='store_true')
+    args = parser.parse_args()
+    try:
+        output = check_output(shlex.split(args.command))
+        lsblk = check_output(shlex.split(args.lsblkcommand))
+    except CalledProcessError as exc:
+        raise SystemExit(exc)
+    # Set the error policy to 'ignore' in order to let tests depending on this
+    # resource to properly match udev properties
+    output = output.decode("UTF-8", errors='ignore')
+    lsblk = lsblk.decode("UTF-8", errors='ignore')
+    list_partitions = False
+    if 'PARTITION' in args.list or 'PARTITION' in args.filter:
+        list_partitions = True
+    udev = UdevadmParser(output, lsblk=lsblk, list_partitions=list_partitions)
+    if args.list:
+        if display_by_categories(udev, args.list, args.short) == 0:
+            raise SystemExit("No devices found")
+    elif args.filter:
+        if filter_by_categories(udev, args.filter) == 0:
+            raise SystemExit("No devices found")
+    else:
+        dump_udev_db(udev)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/org.oniroproject.integration-tests/bin/wifi_nmcli_backup.py b/org.oniroproject.integration-tests/bin/wifi_nmcli_backup.py
new file mode 100755
index 0000000000000000000000000000000000000000..61bd7593cd1b553949b92624542f5ca0b5b92204
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/wifi_nmcli_backup.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+# Copyright 2019 Canonical Ltd.
+# All rights reserved.
+#
+# Written by:
+#   Jonathan Cave <jonathan.cave@canonical.com>
+#
+# Save/Restore NetworkManager wifi connections
+
+import os
+import shutil
+import subprocess as sp
+import sys
+
+from distutils.version import LooseVersion
+
+NM_CON_DIR = '/etc/NetworkManager/system-connections'
+SAVE_DIR = os.path.join(os.path.expandvars(
+    '$PLAINBOX_SESSION_SHARE'), 'stored-system-connections')
+
+
+# versions of NM in cosmic or later allow you to request the keyfile name
+# in `nmcli -t -f <FIELDS> c` output used below
+def legacy_nmcli():
+    cmd = "nmcli -v"
+    output = sp.check_output(cmd, shell=True)
+    version = LooseVersion(output.strip().split()[-1].decode())
+    # check if using an earlier nmcli version with different api
+    # nmcli in cosmic is 1.12.4, bionic is 1.10
+    if version < LooseVersion("1.12.0"):
+        return True
+    return False
+
+
+# Creation of keyfile names can be found in:
+# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/blob/master/libnm-core/nm-keyfile.c#L4046
+# Old format is to replace path separators with '*', in versions that use
+# the new format we can just use the filename supplied by nmcli
+def get_nm_keyfiles():
+    filenames = []
+    if legacy_nmcli():
+        cmd = 'nmcli -t -f TYPE,NAME c'
+        output = sp.check_output(cmd, shell=True)
+        for line in output.decode(sys.stdout.encoding).splitlines():
+            con_type, name = line.strip().split(':')
+            if con_type == '802-11-wireless':
+                filename = name.replace('/', '*')
+                filenames.append(os.path.join(NM_CON_DIR, filename))
+    else:
+        cmd = 'nmcli -t -f TYPE,FILENAME c'
+        output = sp.check_output(cmd, shell=True)
+        for line in output.decode(sys.stdout.encoding).splitlines():
+            con_type, filename = line.strip().split(':')
+            if con_type == '802-11-wireless':
+                filenames.append(filename)
+    return filenames
+
+
+def reload_nm_connections():
+    cmd = 'nmcli c reload'
+    sp.check_call(cmd, shell=True)
+
+
+def save_connections(keyfile_list):
+    if not os.path.exists(SAVE_DIR):
+        os.makedirs(SAVE_DIR)
+    if len(keyfile_list) == 0:
+        print('No stored 802.11 connections to save')
+        return
+    for f in keyfile_list:
+        print('Save connection {}'.format(f))
+        if not os.path.exists(f):
+            print('  No stored connection fount at {}'.format(f))
+            continue
+        print('  Found file {}'.format(f))
+        save_f = shutil.copy(f, SAVE_DIR)
+        print('  Saved copy at {}'.format(save_f))
+
+
+def restore_connections():
+    saved_list = [f for f in os.listdir(
+        SAVE_DIR) if os.path.isfile(os.path.join(SAVE_DIR, f))]
+    if len(saved_list) == 0:
+        print('No stored 802.11 connections found')
+        return
+    for f in saved_list:
+        save_f = os.path.join(SAVE_DIR, f)
+        print('Restore connection {}'.format(save_f))
+        restore_f = shutil.copy(save_f, NM_CON_DIR)
+        print('  Restored file at {}'.format(restore_f))
+        os.remove(save_f)
+        print('  Removed copy from {}'.format(save_f))
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 2:
+        raise SystemExit('ERROR: please specify save or restore')
+    action = sys.argv[1]
+
+    if action == 'save':
+        save_connections(get_nm_keyfiles())
+    elif action == 'restore':
+        restore_connections()
+        reload_nm_connections()
+    else:
+        raise SystemExit('ERROR: unrecognised action')
diff --git a/org.oniroproject.integration-tests/bin/wifi_time2reconnect.py b/org.oniroproject.integration-tests/bin/wifi_time2reconnect.py
new file mode 100755
index 0000000000000000000000000000000000000000..a1b49b57283d4b2e25ce2eabb726ce5db35d1cd2
--- /dev/null
+++ b/org.oniroproject.integration-tests/bin/wifi_time2reconnect.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+import os
+import re
+import sys
+import time
+import subprocess
+from datetime import datetime
+try:
+    from subprocess import DEVNULL  # >= python3.3
+except ImportError:
+    DEVNULL = open(os.devnull, 'wb')
+
+IFACE = None
+TIMEOUT = 30
+
+
+def main():
+    """
+    Check the time needed to reconnect an active WIFI connection
+    """
+    devices = subprocess.getoutput('nmcli dev')
+    match = re.search(r'(\w+)\s+(802-11-wireless|wifi)\s+connected', devices)
+    if match:
+        IFACE = match.group(1)
+    else:
+        print("No active wifi connection detected", file=sys.stderr)
+        return 1
+
+    try:
+        dev_status = subprocess.check_output(
+            ['nmcli', '-t', '-f', 'devices,uuid', 'con', 'status'],
+            stderr=DEVNULL,
+            universal_newlines=True)
+    except subprocess.CalledProcessError:
+        dev_status = subprocess.check_output(
+            ['nmcli', '-t', '-f', 'device,uuid', 'con', 'show'],
+            stderr=DEVNULL,
+            universal_newlines=True)
+    match = re.search(IFACE+':(.*)', dev_status)
+    uuid = None
+    if match:
+        uuid = match.group(1)
+    else:
+        return 1
+
+    subprocess.call(
+        'nmcli dev disconnect iface %s' % IFACE,
+        stdout=open(os.devnull, 'w'),
+        stderr=subprocess.STDOUT,
+        shell=True)
+
+    time.sleep(2)
+    start = datetime.now()
+
+    subprocess.call(
+        'nmcli con up uuid %s --timeout %s' % (uuid, TIMEOUT),
+        stdout=open(os.devnull, 'w'),
+        stderr=subprocess.STDOUT,
+        shell=True)
+
+    delta = datetime.now() - start
+    print('%.2f Seconds' % delta.total_seconds())
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/org.oniroproject.integration-tests/build/bin/clocktest b/org.oniroproject.integration-tests/build/bin/clocktest
new file mode 100755
index 0000000000000000000000000000000000000000..ad7e78a336c4345146c93db3cba20a630c6d1213
Binary files /dev/null and b/org.oniroproject.integration-tests/build/bin/clocktest differ
diff --git a/org.oniroproject.integration-tests/manage.py b/org.oniroproject.integration-tests/manage.py
new file mode 100755
index 0000000000000000000000000000000000000000..e293218d7794b9eedd164e530ced4e72c1099089
--- /dev/null
+++ b/org.oniroproject.integration-tests/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+from plainbox.provider_manager import setup, N_
+
+# You can inject other stuff here but please don't go overboard.
+#
+# In particular, if you need comprehensive compilation support to get
+# your bin/ populated then please try to discuss that with us in the
+# upstream project IRC channel #checkbox on irc.freenode.net.
+
+# NOTE: one thing that you could do here, that makes a lot of sense,
+# is to compute version somehow. This may vary depending on the
+# context of your provider. Future version of PlainBox will offer git,
+# bzr and mercurial integration using the versiontools library
+# (optional)
+
+setup(
+    name='org.oniroproject:tests',
+    version="1.0",
+    description=N_("The org.oniroproject:tests provider"),
+    gettext_domain="org_oniroproject_tests",
+)
diff --git a/org.oniroproject.integration-tests/src/EXECUTABLES b/org.oniroproject.integration-tests/src/EXECUTABLES
new file mode 100644
index 0000000000000000000000000000000000000000..455215570d8e78b397b2da3a7930b5a9a0232d9b
--- /dev/null
+++ b/org.oniroproject.integration-tests/src/EXECUTABLES
@@ -0,0 +1 @@
+clocktest
diff --git a/org.oniroproject.integration-tests/src/Makefile b/org.oniroproject.integration-tests/src/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..5592e14840c9efc58a7167b920f8c15f12a16ff0
--- /dev/null
+++ b/org.oniroproject.integration-tests/src/Makefile
@@ -0,0 +1,11 @@
+.PHONY:
+all: clocktest
+
+.PHONY: clean
+clean:
+	rm -f clocktest
+
+clocktest: CFLAGS += -D_POSIX_C_SOURCE=199309L -D_BSD_SOURCE
+clocktest: LDLIBS += -lrt
+
+CFLAGS += -Wall
diff --git a/org.oniroproject.integration-tests/src/clocktest.c b/org.oniroproject.integration-tests/src/clocktest.c
new file mode 100644
index 0000000000000000000000000000000000000000..cc82d02967d3202bff0bb5b6c569b2a128b388bb
--- /dev/null
+++ b/org.oniroproject.integration-tests/src/clocktest.c
@@ -0,0 +1,165 @@
+/* clocktest.c - check for clock jitter on SMP machines */
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+#include <sys/time.h>
+
+#define __USE_GNU 1
+#include <sched.h>
+
+#define NSEC_PER_SEC    1000000000
+#define MAX_JITTER      (double)0.2
+#define ITERATIONS      10000
+
+#define NSEC(ts) (ts.tv_sec*NSEC_PER_SEC + ts.tv_nsec)
+
+#ifdef OLD_SCHED_SETAFFINITY
+#define setaffinity(mask) sched_setaffinity(0,&mask)
+#else
+#define setaffinity(mask) sched_setaffinity(0,sizeof(mask),&mask)
+#endif
+
+int test_clock_jitter(){
+    cpu_set_t cpumask;
+    struct timespec *time;
+    unsigned long nsec;
+    unsigned slow_cpu, fast_cpu;
+    double jitter;
+    double largest_jitter = 0.0;
+    unsigned cpu, num_cpus, iter;
+    int failures = 0;
+
+    num_cpus = sysconf(_SC_NPROCESSORS_CONF);
+    if (num_cpus == 1) {
+        printf("Single CPU detected. No clock jitter testing necessary.\n");
+        return 0;
+    }
+
+    printf ("Testing for clock jitter on %u cpus\n", num_cpus);
+
+    time=malloc(num_cpus * sizeof(struct timespec));
+
+    for (iter=0; iter<ITERATIONS; iter++){
+        for (cpu=0; cpu < num_cpus; cpu++) {
+            CPU_ZERO(&cpumask); CPU_SET(cpu,&cpumask);
+	        if (setaffinity(cpumask) < 0){
+    	        perror ("sched_setaffinity"); return 1;
+    	    }
+    	    /*
+    	     * by yielding this process should get scheduled on the cpu
+        	 * specified by setaffinity
+	         */
+        	sched_yield();
+            if (clock_gettime(CLOCK_REALTIME, &time[cpu]) < 0) {
+                perror("clock_gettime"); return 1;
+            }
+        }
+
+        slow_cpu = fast_cpu = 0;
+        for (cpu=0; cpu < num_cpus; cpu++) {
+            nsec = NSEC(time[cpu]);
+            if (nsec < NSEC(time[slow_cpu])) { slow_cpu = cpu; }
+            if (nsec > NSEC(time[fast_cpu])) { fast_cpu = cpu; }
+        }
+        jitter = ((double)(NSEC(time[fast_cpu]) - NSEC(time[slow_cpu]))
+                  / (double)NSEC_PER_SEC);
+
+#ifdef DEBUG
+        printf("DEBUG: max jitter for pass %u was %f (cpu %u,%u)\n",
+                iter,jitter,slow_cpu,fast_cpu);
+#endif
+
+    	if (jitter > MAX_JITTER || jitter < -MAX_JITTER){
+	        printf ("ERROR: jitter = %f Jitter must be < 0.2 to pass\n",jitter);
+	        printf ("ERROR: Failed Iteration = %u, Slowest CPU: %u Fastest CPU: %u\n",iter,slow_cpu,fast_cpu);
+            failures++;
+    	}
+	    if (jitter > largest_jitter)
+	        largest_jitter = jitter;
+    }
+
+    if (failures == 0)
+        printf ("PASSED: largest jitter seen was %lf\n",largest_jitter);
+    else
+        printf ("FAILED: %u iterations failed\n",failures);
+
+    return (failures > 0);
+}
+
+/*
+ * This is the original test_clock_direction() function. I've left it here for
+ * reference and in case we wish to resurrect it for some reason. 
+ * This should be removed in the future if the new version pans out.
+int test_clock_direction()
+{
+	time_t starttime = 0;
+	time_t stoptime = 0;
+	int sleeptime = 60;
+	float delta = 0;
+
+	time(&starttime);
+	sleep(sleeptime);
+	time(&stoptime);
+
+	delta = (int)stoptime - (int)starttime - sleeptime;
+	printf("clock direction test: start time %d, stop time %d, sleeptime %u, delta %f\n",
+				(int)starttime, (int)stoptime, sleeptime, delta);
+	if (delta != 0)
+	{
+		printf("FAILED\n");
+		return 1;
+	}
+	/// * otherwise * /
+	printf("PASSED\n");
+	return 0;
+}*/
+
+int test_clock_direction()
+{
+    struct timeval tval_start, tval_stop, tval_result;
+    int sleeptime = 60;
+    int failures = 0;
+    int iteration;
+    double deltas[5];
+    
+    printf("\nTesting clock direction for 5 minutes...\n");
+    /* Because skew can vary, we'll run it 5 times */
+    for (iteration = 0; iteration < 5; iteration++) {
+        /* Replace time() calls with POSIX gettimeofday() */
+        gettimeofday(&tval_start, NULL);
+        sleep(sleeptime);
+        gettimeofday(&tval_stop, NULL);
+ 
+        /* timersub() gives us the delta pretty simply */
+        timersub(&tval_stop, &tval_start, &tval_result);
+        deltas[iteration] = (tval_result.tv_sec - sleeptime) + (tval_result.tv_usec / 1000000.0);
+    }
+
+    for (iteration = 0; iteration < 5; iteration++) {
+        /* if any one iteration fails, test fails */
+        if (deltas[iteration] > 0.01)
+        {
+            printf("FAILED: Iteration %d delta: %f\n", iteration, deltas[iteration]);
+            failures += 1;
+        }
+        /* otherwise */
+        else {
+            printf("PASSED: Iteration %d delta: %f\n", iteration, deltas[iteration]);
+        }
+    }
+    printf("clock direction test: sleeptime %u sec per iteration, failed iterations: %d\n",
+            sleeptime, failures);
+    return (failures > 2);
+}
+
+int main()
+{
+    int failures = test_clock_jitter();
+    if (failures == 0)
+    {
+        failures = test_clock_direction();
+    }
+    return failures;
+}
diff --git a/org.oniroproject.integration-tests/units/Audio/category.pxu b/org.oniroproject.integration-tests/units/Audio/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..54ff1b4ea64cbadf2d99e28d616216068b981880
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Audio/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Audio
+_name: Audio tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Audio/jobs-Audio.pxu b/org.oniroproject.integration-tests/units/Audio/jobs-Audio.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..7d4f097f40afb580b76dcd32f7a50b5b71d40517
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Audio/jobs-Audio.pxu
@@ -0,0 +1,33 @@
+unit: job
+id: audio/detect-playback-devices
+_summary: HW_AUDIO_001 -  Check that at least one audio playback device exits
+plugin: shell
+category_id: Audio
+command:
+ COUNT=$(alsa_pcm_info.py | grep -c Playback)
+ echo "Count: $COUNT"
+ if [ "$COUNT" -eq 0 ]; then
+  exit 1
+ fi
+
+id: audio/detect-capture-devices
+_summary: HW_AUDIO_002 -  Check that at least one audio capture device exists
+plugin: shell
+category_id: Audio
+command:
+ COUNT=$(alsa_pcm_info.py | grep -c Capture)
+ echo "Count: $COUNT"
+ if [ "$COUNT" -eq 0 ]; then
+   exit 1
+ fi
+
+id: audio/alsa-loopback-automated
+_summary: HW_AUDIO_003 -  Captured sound matches played one (automated) 
+_purpose:
+ Check if sound that is 'hearable' by capture device
+plugin: shell
+depends: audio/detect-playback-devices audio/detect-capture-devices
+user: root
+environ: ALSA_CONFIG_PATH LD_LIBRARY_PATH ALSADEVICE
+command: alsa_test loopback -d 5
+category_id: Audio
diff --git a/org.oniroproject.integration-tests/units/Audio/test-plan-Audio.pxu b/org.oniroproject.integration-tests/units/Audio/test-plan-Audio.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..5dd66853f5a1a0259e9e7c4ecd6f5a08fe19b427
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Audio/test-plan-Audio.pxu
@@ -0,0 +1,8 @@
+id: audio-automated
+unit: test plan
+_name: Audio (Automated)
+_description: Automated audio tests for Snappy Ubuntu Core devices
+include:
+    audio/detect-playback-devices
+    audio/detect-capture-devices
+    audio/alsa-loopback-automated
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Bluetooth/category.pxu b/org.oniroproject.integration-tests/units/Bluetooth/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..0cced93c518431341c1c77aca144f5ada73cc979
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Bluetooth/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Bluetooth
+_name: Bluetooth tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Bluetooth/jobs-Bluetooth.pxu b/org.oniroproject.integration-tests/units/Bluetooth/jobs-Bluetooth.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..a857c46e0f2d1e614e42f2785559a2a7f778e4c0
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Bluetooth/jobs-Bluetooth.pxu
@@ -0,0 +1,92 @@
+unit: job
+plugin: shell
+id: bluetooth/detect
+category_id: Bluetooth
+_summary: HW_BT_001 - Make sure at least one bluetooth device is detected
+command:
+  bt_list_adapters.py && udev_resource.py -f BLUETOOTH
+
+
+plugin: shell
+unit: job
+id: bluetooth/bluez-controller-detect
+category_id: Bluetooth
+_summary: HW_BT_002 - Check bluez lists a controller if rfkill detects one
+user: root
+depends: bluetooth/detect
+template-engine: jinja2
+requires:
+  package.name == 'bluez' or snap.name == 'bluez'
+  {%- if __on_ubuntucore__ %}
+  connections.slot == 'bluez:service' and connections.plug == '{{ __system_env__["SNAP_NAME"] }}:bluez'
+  {% endif -%}
+command:
+  bluez_list_adapters.py
+
+
+plugin: shell
+unit: template
+template-resource: bluez-internal-rfcomm-tests
+template-unit: job
+id: bluetooth/bluez-internal-rfcomm-tests_{bluez-internal-rfcomm-test}
+category_id: Bluetooth
+_summary:HW_BT_003 - BlueZ-{bluez-internal-rfcomm-test}
+_description:
+ Runs a specific test from the rfcomm test suite
+user: root
+command:
+ rfcomm-tester -p "{bluez-internal-rfcomm-test}"
+
+
+plugin: shell
+unit: template
+template-resource: bluez-internal-hci-tests
+template-unit: job
+id: bluetooth/bluez-internal-hci-tests_{bluez-internal-hci-test}
+category_id: Bluetooth
+_summary: HW_BT_004 - BlueZ-{bluez-internal-hci-test}
+_description:
+ Runs a specific test from the hci test suite
+user: root
+command:
+ hci-tester -p "{bluez-internal-hci-test}"
+
+
+plugin: shell
+unit: template
+template-resource: bluez-internal-uc-tests
+template-unit: job
+id: bluetooth/bluez-internal-uc-tests_{bluez-internal-uc-test}
+category_id: Bluetooth
+_summary: HW_BT_005 - BlueZ-{bluez-internal-uc-test}
+_description:
+ Runs a specific test from the user channel test suite
+user: root
+command:
+ userchan-tester -p "{bluez-internal-uc-test}"
+
+
+plugin: shell
+unit: template
+template-resource: bluez-internal-bnep-tests
+template-unit: job
+id: bluetooth/bluez-internal-bnep-tests_{bluez-internal-bnep-test}
+category_id: Bluetooth
+_summary: HW_BT_006 - BlueZ-{bluez-internal-bnep-test}
+_description:
+ Runs a specific test from the bnep test suite
+user: root
+command:
+ bnep-tester -p "{bluez-internal-bnep-test}"
+
+
+plugin: shell
+unit: template
+template-resource: device
+template-unit: job
+id: bluetooth4/beacon_eddystone_url_{interface}
+_summary: HW_BT_007 - Test system can get beacon EddyStone URL advertisements on the {interface} adapter 
+command:
+ checkbox-support-eddystone_scanner -D {interface}
+user: root
+category_id: Bluetooth
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Bluetooth/test-plan-Bluetooth.pxu b/org.oniroproject.integration-tests/units/Bluetooth/test-plan-Bluetooth.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..ef517fea8a3f569d8c7209777585bf3776929a2e
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Bluetooth/test-plan-Bluetooth.pxu
@@ -0,0 +1,19 @@
+id: bluez-automated
+unit: test plan
+_name: Bluetooth (Autometed)
+_description:
+ Automated tests for bluez
+include:
+    bluetooth/detect
+    bluetooth/bluez-controller-detect
+    bluetooth/bluez-internal-rfcomm-tests_.*
+    bluetooth/bluez-internal-hci-tests_.*
+    bluetooth/bluez-internal-uc-tests_.*
+    bluetooth/bluez-internal-bnep-tests_.*
+    bluetooth4/beacon_eddystone_url_.*
+bootstrap_include:
+    device
+    bluez-internal-rfcomm-tests
+    bluez-internal-hci-tests
+    bluez-internal-uc-tests
+    bluez-internal-bnep-tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CAM/category.pxu b/org.oniroproject.integration-tests/units/CAM/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..d3054c60142eaeeface6cafa04148b2e087fbb12
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CAM/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: CAM
+_name: CAM tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CAM/jobs-CAM.pxu b/org.oniroproject.integration-tests/units/CAM/jobs-CAM.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..90abdf3d9ccf92fd53aebfe2b9109fddef59886b
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CAM/jobs-CAM.pxu
@@ -0,0 +1,29 @@
+plugin: shell
+category_id: CAM
+id: camera/detect
+command:
+  camera_test.py detect
+_summary: HW_CAM_001 - Automated test attempts to detect a camera
+user: root
+
+unit: template
+template-resource: device
+template-filter: device.category == 'CAPTURE' and device.name != ''
+template-unit: job
+plugin: shell
+template-engine: jinja2
+category_id: CAM
+id: camera/multiple-resolution-images_{{ name }}
+_summary: HW_CAM_002 - Takes multiple pictures based on the resolutions supported by the camera and validates their size and that they are of a valid format.
+depends: camera/detect
+requires:
+  {%- if __on_ubuntucore__ %}
+ executable.name == 'fswebcam'
+ {%- else %}
+ package.name == 'fswebcam' or package.name == 'gir1.2-gst-plugins-base-1.0'
+ {% endif -%}
+command:
+  camera_test.py resolutions -d /dev/{{ name }}
+_description:
+  Takes multiple pictures based on the resolutions supported by the camera and
+  validates their size and that they are of a valid format.
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CAM/test-plan-CAM.pxu b/org.oniroproject.integration-tests/units/CAM/test-plan-CAM.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..97f111c948ada8b82d7919bf08fcbcb7c6afe16b
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CAM/test-plan-CAM.pxu
@@ -0,0 +1,9 @@
+id: camera-cert-automated
+unit: test plan
+_name: Camera (Automated)
+_description: Camera tests (automated)
+include:
+ camera/detect                                     
+ camera/multiple-resolution-images_.*              
+bootstrap_include:
+ device
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CPU/category.pxu b/org.oniroproject.integration-tests/units/CPU/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..5e3e52712dfbae63e46d2d24e85d08064ac2c229
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CPU/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: CPU
+_name: CPU tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CPU/jobs-CPU.pxu b/org.oniroproject.integration-tests/units/CPU/jobs-CPU.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..4985dc71846e91ecf9371cb1bca3c0d35f20c7e6
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CPU/jobs-CPU.pxu
@@ -0,0 +1,148 @@
+plugin: shell
+category_id: CPU
+id: cpu/scaling_test
+requires:
+ executable.name == 'fwts'
+ 'userspace' in cpuinfo.governors
+ cpuinfo.platform not in ("ppc64el", "s390x")
+user: root
+environ: PLAINBOX_SESSION_SHARE LD_LIBRARY_PATH SNAP
+command:
+ if [[ -v SNAP ]]; then
+     export LD_LIBRARY_PATH=$SNAP/usr/lib/fwts:$LD_LIBRARY_PATH
+ fi
+ checkbox-support-fwts_test -t cpufreq -l "${PLAINBOX_SESSION_SHARE}"/scaling_test.log
+_summary: BENCH_CPU_001 - CPU utilization on an idle system
+ Test the CPU scaling capabilities
+_description:
+ Use Firmware Test Suite (fwts cpufreq) to test the scaling capabilities of the
+ CPU.
+
+plugin: attachment
+category_id: CPU
+id: cpu/scaling_test-log-attach
+depends: cpu/scaling_test
+command: [[ -e "${PLAINBOX_SESSION_SHARE}"/scaling_test.log ]] && cat "${PLAINBOX_SESSION_SHARE}"/scaling_test.log
+_summary: BENCH_CPU_002 - Disk utilization on an idle system
+ Attach CPU scaling capabilities log
+_description:
+ Attaches the log generated by cpu/scaling_test to the results submission.
+
+plugin: shell
+category_id: CPU
+id: cpu/maxfreq_test
+requires: executable.name == 'fwts'
+ cpuinfo.platform in ("i386", "x86_64")
+user: root
+environ: LD_LIBRARY_PATH SNAP
+command:
+ if [[ -v SNAP ]]; then
+     export LD_LIBRARY_PATH=$SNAP/usr/lib/fwts:$LD_LIBRARY_PATH
+ fi
+ checkbox-support-fwts_test -t maxfreq -l "$PLAINBOX_SESSION_SHARE"/maxfreq_test.log
+_summary: BENCH_CPU_003 - Test the CPU scaling capabilities
+ Test that the CPU can run at its max frequency
+_description:
+ Use the Firmware Test Suite (fwts cpufreq) to ensure that the CPU can run at
+ its maximum frequency.
+
+plugin: attachment
+category_id: CPU
+id: cpu/maxfreq_test-log-attach
+depends: cpu/maxfreq_test
+command: [ -e "$PLAINBOX_SESSION_SHARE"/maxfreq_test.log ] && cat "$PLAINBOX_SESSION_SHARE"/maxfreq_test.log
+_summary: BENCH_CPU_004 - Attach CPU scaling capabilities log
+ Attach CPU max frequency log
+_description:
+ Attaches the log generated by cpu/maxfreq_test to the results submission.
+
+plugin: shell
+category_id: CPU
+id: cpu/clocktest
+command: clocktest
+_summary: BENCH_CPU_005 - Test that the CPU can run at its max frequency
+_description:
+ Runs a test for clock jitter on SMP machines.
+
+plugin: shell
+category_id: CPU
+id: cpu/offlining_test
+user: root
+command: cpu_offlining.py
+_summary: BENCH_CPU_006 - Attach CPU max frequency log
+ Test offlining of each CPU core
+_description:
+ Attempts to offline each core in a multicore system.
+requires: cpuinfo.platform not in ("aarch64", "armv7l")
+
+plugin: shell
+category_id: CPU
+id: cpu/topology
+requires: int(cpuinfo.count) > 1 and (cpuinfo.platform == 'i386' or cpuinfo.platform == 'x86_64')
+command: cpu_topology.py
+_summary: BENCH_CPU_007 - Tests the CPU for clock jitter
+ Check CPU topology for accuracy between proc and sysfs
+_description:
+ Parses information about CPU topology provided by proc and sysfs and checks
+ that they are consistent.
+
+unit: template
+template-resource: cpuinfo
+template-filter: cpuinfo.platform == 'armv7l'
+template-unit: job
+plugin: shell
+category_id: CPU
+id: cpu/arm_vfp_support_{platform}
+user: root
+command:
+  echo "{other}" | grep "vfp\|vfpv3\|vfpv4\|vfpd32"
+_summary: BENCH_CPU_008 - Test offlining of each CPU core
+ Validate that the Vector Floating Point Unit is running on {platform} device
+_description:
+ Validate that the Vector Floating Point Unit is running on {platform} device.
+
+plugin:shell
+category_id: CPU
+id: cpu/cstates
+requires:
+ executable.name == 'fwts'
+ cpuinfo.platform not in ("aarch64", "armv7l", "s390x")
+user: root
+_summary: BENCH_CPU_009 - Check CPU topology for accuracy between proc and sysfs
+ Run C-States tests
+_description:
+ Uses the Firmware Test Suite (fwts) to test the power saving states of the CPU.
+environ: PLAINBOX_SESSION_SHARE LD_LIBRARY_PATH SNAP
+command:
+ if [[ -v SNAP ]]; then
+     export LD_LIBRARY_PATH=$SNAP/usr/lib/fwts:$LD_LIBRARY_PATH
+ fi
+ checkbox-support-fwts_test -l "$PLAINBOX_SESSION_SHARE"/fwts_cstates_results.log -t cstates
+
+plugin: attachment
+category_id: CPU
+id: cpu/cstates_results.log
+requires: cpuinfo.platform not in ("aarch64", "armv7l", "s390x")
+after: cpu/cstates
+command:
+ [ -e "${PLAINBOX_SESSION_SHARE}"/fwts_cstates_results.log ] && cat "${PLAINBOX_SESSION_SHARE}"/fwts_cstates_results.log
+_summary: BENCH_CPU_010 - Validate that the Vector Floating Point Unit is running on {platform} device
+ Attach C-States test log
+_description:
+ Attaches the FWTS desktop diagnosis results log to the submission.
+
+plugin: shell
+category_id: CPU
+id: benchmarks/system/cpu_on_idle
+_summary: BENCH_CPU_011 - Run C-States tests
+requires: package.name == 'sysstat'
+command: iostat -x -m 1 10 | python3 -c 'import sys, re; lines="".join(sys.stdin.readlines()); l=[float(n) for n in (re.findall("idle\n.*?(\S+)\n", lines))]; print(sum(l)/len(l),"%")'
+_description: CPU utilization on an idle system.
+
+plugin: shell
+category_id: CPU
+id: benchmarks/system/disk_on_idle
+_summary: BENCH_CPU_012 - Attach C-States test log
+requires: package.name == 'sysstat'
+command: iostat -x -m 1 10 | python3 -c 'import sys, re; lines="".join(sys.stdin.readlines()); l=[float(n) for n in (re.findall("util\n.*?(\S+)\n", lines))]; print(sum(l)/len(l),"%")'
+_description: Disk utilization on an idle system.
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/CPU/test-plan-CPU.pxu b/org.oniroproject.integration-tests/units/CPU/test-plan-CPU.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..82adb8d4edc52e1b0093a4ccac138794f4b54253
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/CPU/test-plan-CPU.pxu
@@ -0,0 +1,23 @@
+id: cpu-cert-automated
+unit: test plan
+_name: CPU (Automated)
+_description: CPU tests (automated)
+include:                        
+  cpu/scaling_test                               
+  cpu/scaling_test-log-attach
+  cpu/maxfreq_test                               
+  cpu/maxfreq_test-log-attach
+  cpu/offlining_test                             
+  cpu/topology                                   
+  cpu/clocktest
+  cpu/cstates                                    
+  cpu/cstates_results.log
+  cpu/arm_vfp_support_.*
+  
+id: cpu-on-idle
+unit: test plan
+_name: CPU on idle (Automated)
+_description: CPU tests on idle (automated)
+include:                        
+  cpu/cpu_on_idle
+  cpu/disk_on_idle
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Cellular/category.pxu b/org.oniroproject.integration-tests/units/Cellular/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..06be8071805e3ed571b29e1549669267568af18a
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Cellular/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Cellular
+_name: Cellular tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Cellular/jobs-Cellular.pxu b/org.oniroproject.integration-tests/units/Cellular/jobs-Cellular.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..5ee06a4df3fec1a2521d2c99d615a83629dc73d5
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Cellular/jobs-Cellular.pxu
@@ -0,0 +1,61 @@
+unit: job
+plugin: shell
+category_id: Cellular
+id: mobilebroadband/gsm_connection
+_summary: SF_NT_Cellular_001 - Creates a mobile broadband connection for a GSM based modem and checks the connection to ensure it's working.
+requires:
+    package.name == 'network-manager'
+    package.name == 'modemmanager'
+    mobilebroadband.gsm == 'supported'
+user: root
+environ: GSM_CONN_NAME GSM_APN GSM_USERNAME GSM_PASSWORD
+command:
+  if [ -n "${GSM_APN}" ]; then
+      # shellcheck disable=SC2064
+      trap "nmcli con delete id $GSM_CONN_NAME" EXIT
+      create_connection.py mobilebroadband gsm \
+      "$([ -n "${GSM_APN}" ] && echo "--apn=$GSM_APN")" \
+      "$([ -n "${GSM_CONN_NAME}" ] &&  echo "--name=$GSM_CONN_NAME")" \
+      "$([ -n "${GSM_USERNAME}" ] && echo "--username=$GSM_USERNAME")" \
+      "$([ -n "${GSM_PASSWORD}" ] && echo "--password=$GSM_PASSWORD")" || exit 1
+  fi
+  INTERFACE=$( (nmcli -f GENERAL -t dev list 2>/dev/null || nmcli -f GENERAL -t dev show) | tr '\n' ' ' | grep -oP 'TYPE:\Kgsm.*' | sed 's/GENERAL.TYPE:.*//' | grep -oP 'GENERAL.IP-IFACE:\K\S*')
+  echo "connected GSM interface seems to be $INTERFACE"
+  [ -z "$INTERFACE" ] && exit 1
+  curl http://start.ubuntu.com/connectivity-check.html --interface "$INTERFACE"
+  EXIT_CODE=$?
+  if [ -n "${GSM_APN}" ] && [ "$(nmcli dev status | awk '/gsm/ {print $3}')" == "connected" ]; then
+          nmcli con down id "$([ "${GSM_CONN_NAME}" ] && echo "$GSM_CONN_NAME" || echo "MobileBB")"
+  fi
+  exit $EXIT_CODE
+_description: Creates a mobile broadband connection for a GSM based modem and checks the connection to ensure it's working. 
+
+plugin: shell
+category_id: Cellular
+id: mobilebroadband/cdma_connection
+_summary:SF_NT_Cellular_002 - Creates a mobile broadband connection for a CDMA based modem and checks the connection to ensure it's working.
+requires:
+    package.name == 'network-manager'
+    package.name == 'modemmanager'
+    mobilebroadband.cdma == 'supported'
+user: root
+environ: CDMA_CONN_NAME CDMA_USERNAME CDMA_PASSWORD
+command:
+  if [ -n "${CDMA_USERNAME}" ]; then
+      # shellcheck disable=SC2064
+      trap "nmcli con delete id $CDMA_CONN_NAME" EXIT
+      create_connection.py mobilebroadband cdma \
+      "$([ -n "${CDMA_CONN_NAME}" ] &&  echo "--name=$CDMA_CONN_NAME")" \
+      "$([ -n "${CDMA_USERNAME}" ] && echo "--username=$CDMA_USERNAME")" \
+      "$([ -n "${CDMA_PASSWORD}" ] && echo "--password=$CDMA_PASSWORD")" || exit 1
+  fi
+  INTERFACE=$( (nmcli -f GENERAL -t dev list 2>/dev/null || nmcli -f GENERAL -t dev show) | tr '\n' ' ' | grep -oP 'TYPE:\Kcdma.*' | sed 's/GENERAL.TYPE:.*//' | grep -oP 'GENERAL.IP-IFACE:\K\S*')
+  echo "connected CDMA interface seems to be $INTERFACE"
+  [ -z "$INTERFACE" ] && exit 1
+  curl http://start.ubuntu.com/connectivity-check.html --interface "$INTERFACE"
+  EXIT_CODE=$?
+  if [ -n "${CDMA_USERNAME}" ] && [ "$(nmcli dev status | awk '/cdma/ {print $3}')" == "connected" ]; then
+          nmcli con down id "$([ "${CDMA_CONN_NAME}" ] && echo "$CDMA_CONN_NAME" || echo "MobileBB")"
+  fi
+  exit $EXIT_CODE
+_description: Creates a mobile broadband connection for a CDMA based modem and checks the connection to ensure it's working.
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Cellular/test-plan-Cellular.pxu b/org.oniroproject.integration-tests/units/Cellular/test-plan-Cellular.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..a0f6c518c98244eebc3c37088cd8836c06be2349
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Cellular/test-plan-Cellular.pxu
@@ -0,0 +1,7 @@
+id: mobilebroadband-cert-automated
+unit: test plan
+_name: Cellular (Automated)
+_description: Mobile broadband tests (automated)
+include:
+    mobilebroadband/gsm_connection 
+    mobilebroadband/cdma_connection
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Ethernet/category.pxu b/org.oniroproject.integration-tests/units/Ethernet/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..e5ac2924600ed6285e0439cc8d69cb96e85d76fa
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Ethernet/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Ethernet
+_name: Ethernet tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Ethernet/jobs-Ethernet.pxu b/org.oniroproject.integration-tests/units/Ethernet/jobs-Ethernet.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..c8329f22db29efb2d640d3f71ece3abcfa44b357
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Ethernet/jobs-Ethernet.pxu
@@ -0,0 +1,31 @@
+unit: job
+plugin: shell
+category_id: Ethernet
+id: ethernet/detect
+command: network_device_info.py detect NETWORK
+_summary: HW_ETH_Detection_001 - Detect if at least one ethernet device is detected
+_description:
+ Test to detect and return information about available network controllers on
+ the system under test.
+
+
+plugin: shell
+category_id: Ethernet
+id: ethernet/ping_{interface}
+command: gateway_ping_test.py -v --interface {interface}
+_summary: HW_ETH_Detection_002 - Can ping another machine over Ethernet port
+_description: Check Ethernet works by pinging another machine
+
+
+plugin: user-interact
+id: ethernet/hotplug-{{ interface }}
+_summary: HW_ETH_Hotplugging_001 - Check that hotplugging works on port {{ interface }}
+_description:
+ Check that hotplugging works on port {{ interface }}
+_steps:
+ 1. Begin the test.
+ 2. Follow the instructions on the screen.
+command:
+ eth_hotplugging.py {{ interface }} && gateway_ping_test.py -v --interface {{ interface }}
+category_id: Ethernet
+user: root
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Ethernet/packaging-Ethernet.pxu b/org.oniroproject.integration-tests/units/Ethernet/packaging-Ethernet.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..efda351ca8735e3b3a3355c9a4067acd4f181a5d
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Ethernet/packaging-Ethernet.pxu
@@ -0,0 +1,8 @@
+unit: packaging meta-data
+os-id: debian
+Depends: kmod
+
+# This is needed by network_device_info.py
+unit: packaging meta-data
+os-id: debian
+Depends: python3-natsort
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Ethernet/test-plan-Ethernet.pxu b/org.oniroproject.integration-tests/units/Ethernet/test-plan-Ethernet.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..8ee969a2c4a96fe9a98bba4df0aa316ac9f33ae8
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Ethernet/test-plan-Ethernet.pxu
@@ -0,0 +1,19 @@
+id: ethernet-automated
+unit: test plan
+_name: Ethernet (Autometed)
+_description: Automated ethernet tests for Ubuntu Core devices
+estimated_duration: 1m
+include:
+    ethernet/detect
+    ethernet/ping_.*
+bootstrap_include:
+    device
+
+id: ethernet-cert-manual
+unit: test plan
+_name: Ethernet (Manual)
+_description: Ethernet tests (manual)
+include:
+    ethernet/hotplug-.*
+bootstrap_include:
+    device
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/GPIO/category.pxu b/org.oniroproject.integration-tests/units/GPIO/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..85751ee8d38834455b2176e124a73e2fc98a498e
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPIO/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: GPIO
+_name: GPIO tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/GPIO/jobs-GPIO.pxu b/org.oniroproject.integration-tests/units/GPIO/jobs-GPIO.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..aebd2a0f30c0918240b080e6ef393aa813874a9a
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPIO/jobs-GPIO.pxu
@@ -0,0 +1,17 @@
+unit: job
+plugin: shell
+id: gpio/sysfs_loopback_pairs_{model}
+_summary: HW_GPIO_001 - Test GPIO lines exposed on headers can be controlled via sysfs
+user: root
+category_id: GPIO
+command:
+  gpio_sysfs_loopback.py {model}
+
+
+plugin: shell
+id: gpio/gpiomem_loopback_pairs_{model}
+_summary: HW_GPIO_002 - Test GPIO lines exposed on headers can be controlled via /dev/gpiomem
+user: root
+category_id: GPIO
+command:
+  gpio_gpiomem_loopback.py {model}
diff --git a/org.oniroproject.integration-tests/units/GPIO/test-plan-GPIO.pxu b/org.oniroproject.integration-tests/units/GPIO/test-plan-GPIO.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..9060acbee3de6e264a53cbfb5fd36fb97b23847d
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPIO/test-plan-GPIO.pxu
@@ -0,0 +1,10 @@
+id: gpio-automated
+unit: test plan
+_name: GPIO (Automated)
+_description: Automated GPIO tests for Ubuntu Core devices
+bootstrap_include:
+    model_assertion
+    dmi
+include:
+    gpio/sysfs_loopback_pairs_.*
+    gpio/gpiomem_loopback_pairs_.*
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/GPU/category.pxu b/org.oniroproject.integration-tests/units/GPU/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..4290522cb9cd771479ce3a36721a03d5390af1c0
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPU/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: GPU
+_name: GPU tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/GPU/jobs-GPU.pxu b/org.oniroproject.integration-tests/units/GPU/jobs-GPU.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..86ebd0e44c2d002b0df503f9c7f1af55a67b74aa
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPU/jobs-GPU.pxu
@@ -0,0 +1,72 @@
+unit: job
+plugin: shell
+category_id: GPU
+id: graphics/{index}_auto_switch_card_{product_slug}
+_summary: HW_GPU_001 - Switch GPU to {vendor} {product} and reboot
+_description:
+ Switch GPU to {vendor} {product} and reboot the machine
+user: root
+command:
+ {switch_to_cmd}
+ pm_test.py --silent --checkbox-respawn-cmd "$PLAINBOX_SESSION_SHARE"/__respawn_checkbox reboot --log-level=debug --log-dir="$PLAINBOX_SESSION_SHARE"
+
+
+plugin: shell
+category_id: GPU
+id: graphics/VESA_drivers_not_in_use
+command: cat /var/log/Xorg.0.log ~/.local/share/xorg/Xorg.0.log 2>&1 | perl -e '$a=0;while(<>){$a++ if /Loading.*vesa_drv\.so/;$a-- if /Unloading.*vesa/&&$a}exit 1 if $a'
+_description: Check that VESA drivers are not in use
+_summary: HW_GPU_002 - Check that VESA drivers are not in use
+
+
+plugin: shell
+category_id: GPU
+id: graphics/{index}_driver_version_{product_slug}
+command:
+ # shellcheck disable=SC1091
+ source graphics_env.sh {driver} {index}
+ if [[ $XDG_SESSION_TYPE == "wayland" ]]
+ then
+   inxi_snapshot -Gazy
+ else
+   graphics_driver.py
+ fi
+_description: Parses Xorg.0.log and discovers the running X driver and version for the {vendor} {product} graphics card
+_summary: HW_GPU_003 - Test X driver/version for {vendor} {product}
+
+
+plugin: shell
+template-resource: graphics_card
+category_id: GPU
+id: graphics/{index}_gl_support_{product_slug}
+command:
+  "$CHECKBOX_RUNTIME"/usr/lib/nux/unity_support_test -p 2>&1
+_description: Check that {vendor} {product} hardware is able to run a desktop session (OpenGL)
+_summary: HW_GPU_004 - Test OpenGL support for {vendor} {product}
+user: root
+
+
+plugin: shell
+category_id: GPU
+id: graphics/{index}_minimum_resolution_{product_slug}
+command:
+ # shellcheck disable=SC1091
+ source graphics_env.sh {driver} {index}
+ resolution_test.py --horizontal 800 --vertical 600
+_summary: HW_GPU_005 - Test that {vendor} {product} meets minimum resolution requirement
+_description:
+ Ensure the current resolution meets or exceeds the recommended minimum
+ resolution (800x600) on the {vendor} {product} graphics card. See here for details:
+ https://help.ubuntu.com/community/Installation/SystemRequirements
+
+
+plugin: shell
+category_id: GPU
+id: suspend/{index}_resolution_before_suspend_{product_slug}_auto
+after: graphics/{index}_auto_switch_card_{product_slug}
+_summary: HW_GPU_006 - Record the current resolution before suspending.
+_description: Record the current resolution before suspending.
+command:
+ # shellcheck disable=SC1091
+ source graphics_env.sh {driver} {index}
+ xrandr -q | grep "[*]" | awk '{{print $1}}' > "$PLAINBOX_SESSION_SHARE"/{index}_resolution_before_suspend.txt
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/GPU/packaging-GPU.pxu b/org.oniroproject.integration-tests/units/GPU/packaging-GPU.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..a527178288c39a4f3e1f18e689ca17da9e29c9f6
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPU/packaging-GPU.pxu
@@ -0,0 +1,8 @@
+unit: packaging meta-data
+os-id: ubuntu
+os-version-id: 22.04
+Depends: gnome-randr
+
+unit: packaging meta-data
+os-id: debian
+Depends: gnome-screenshot
diff --git a/org.oniroproject.integration-tests/units/GPU/test-plan-GPU.pxu b/org.oniroproject.integration-tests/units/GPU/test-plan-GPU.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..54b01e929508322e41be8b8c08927b7113de2f9a
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/GPU/test-plan-GPU.pxu
@@ -0,0 +1,14 @@
+id: graphics-integrated-gpu-cert-automated
+unit: test plan
+_name: GPU Integrated (Automated)
+_description:
+ Graphics tests (integrated GPU) (Automated)
+include:
+ graphics/1_auto_switch_card_.*                 
+ graphics/VESA_drivers_not_in_use               
+ graphics/1_driver_version_.*                   
+ graphics/1_gl_support_.*                       
+ graphics/1_minimum_resolution_.*
+ suspend/1_resolution_before_suspend_.*_auto    
+bootstrap_include:
+    graphics_card
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/I2C/category.pxu b/org.oniroproject.integration-tests/units/I2C/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..1eff8fc03d803e573a302c2acdf6135b5876a445
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/I2C/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: I2C
+_name: I2C tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/I2C/jobs-I2C.pxu b/org.oniroproject.integration-tests/units/I2C/jobs-I2C.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..3b3dbbad57cd6aa112ae2e5ce948ca4014b74948
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/I2C/jobs-I2C.pxu
@@ -0,0 +1,31 @@
+unit: job
+id: i2c/i2c-bus-detect
+_summary: HW_I2C_001 - Check presence of an I2C bus
+_description:
+  If an expected number of I2C buses is provided, the job will verify the
+  detected number is correct. If the expected number of buses is not provided
+  the job will pass if at least one I2C bus is detected.
+command:
+  if [ -z "${I2C_BUS_NUMBER+x}" ]; then
+    i2c_driver_test.py bus
+  else
+    i2c_driver_test.py bus -b "$I2C_BUS_NUMBER"
+  fi
+user: root
+plugin: shell
+category_id: I2C
+environ: I2C_BUS_NUMBER
+requires: manifest.has_i2c == 'True'
+imports: from com.canonical.plainbox import manifest
+
+unit: job
+id: i2c/i2c-device-detect
+_summary: HW_I2C_002 - Check if any I2C devices can be detected
+_description:
+  The test will pass if there's at least one I2C device detected on any I2C bus.
+command:
+  i2c_driver_test.py device
+user: root
+plugin: shell
+category_id: I2C
+depends: i2c/i2c-bus-detect
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/I2C/test-plan-I2C.pxu b/org.oniroproject.integration-tests/units/I2C/test-plan-I2C.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..a2463b3a67039f1a937dd2b38b0e78c34567e723
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/I2C/test-plan-I2C.pxu
@@ -0,0 +1,7 @@
+id: i2c-automated
+unit: test plan
+_name: I2C (Automated)
+_description: Automated I2C tests for Ubuntu Core devices
+include:
+    i2c/i2c-bus-detect
+    i2c/i2c-device-detect
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Memory/category.pxu b/org.oniroproject.integration-tests/units/Memory/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..154c49ca2016504bd7c3c61fcb1069e5e2683a48
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Memory/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Memory
+_name: Memory tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Memory/jobs-Memory.pxu b/org.oniroproject.integration-tests/units/Memory/jobs-Memory.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..25b8d22c275a4189765a6bd2e2a5944990552afb
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Memory/jobs-Memory.pxu
@@ -0,0 +1,10 @@
+unit: job
+plugin: shell
+category_id: Memory
+id: memory/info
+user: root
+command: memory_compare.py
+_summary: BENCH_MEM_001 - Check amount of memory reported by meminfo against DMI
+_description:
+ This test checks the amount of memory which is reporting in meminfo against
+ the size of the memory modules detected by DMI.
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Memory/test-plan-Memory.pxu b/org.oniroproject.integration-tests/units/Memory/test-plan-Memory.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..cc39e574840f5ed006a20f1fda26209b0196815d
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Memory/test-plan-Memory.pxu
@@ -0,0 +1,6 @@
+id: memory-automated
+unit: test plan
+_name: Memory (Autometed)
+_description: Automated memory tests for Ubuntu Core devices
+include:
+    memory/info
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Micro-SD-slot/category.pxu b/org.oniroproject.integration-tests/units/Micro-SD-slot/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..023f7dfb977e6d8dcdc6915f4b474d6c23947609
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Micro-SD-slot/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: SD
+_name: SD tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Micro-SD-slot/jobs-SD.pxu b/org.oniroproject.integration-tests/units/Micro-SD-slot/jobs-SD.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..047b0aa63c8058cd2240df031e5d6ce04c7c60a7
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Micro-SD-slot/jobs-SD.pxu
@@ -0,0 +1,196 @@
+unit: job
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sd-insert
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher insertion mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard insert sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_001 - Test that insertion of an SD card is detected
+_description:
+ PURPOSE:
+     This test will check that the systems media card reader can
+     detect the insertion of an UNLOCKED Secure Digital (SD) media card
+ STEPS:
+     1. Commence the test and then insert an UNLOCKED SD card into the reader.
+        (Note: this test will time-out after 20 seconds.)
+     2. Do not remove the device after this test.
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
+
+plugin: shell
+template-engine: jinja2
+category_id: SD
+id: mediacard/sd-storage
+depends: mediacard/sd-insert
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-usb_read_write
+ {%- else %}
+     removable_storage_test.py -s 268400000 --memorycard sdio usb scsi --auto-reduce-size
+ {% endif -%}
+_summary: HW_SLOTSD_002 - Test reading & writing to a SD Card
+_description:
+ This test is automated and executes after the mediacard/sd-insert
+ test is run. It tests reading and writing to the SD card.
+
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sd-remove
+depends: mediacard/sd-storage
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher removal mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard remove sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_003 - Test that removal of an SD card is detected
+_description:
+ PURPOSE:
+     This test will check that the system correctly detects
+     the removal of an SD card from the systems card reader.
+ STEPS:
+     1. Commence the test and then remove the SD card from the reader.
+        (Note: this test will time-out after 20 seconds.)
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
+
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdhc-insert
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher insertion mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard insert sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_004 - Test that insertion of an SDHC card is detected
+_description:
+ PURPOSE:
+     This test will check that the systems media card reader can
+     detect the insertion of a UNLOCKED Secure Digital High-Capacity
+     (SDHC) media card
+ STEPS:
+     1. Commence the test and then insert an UNLOCKED SDHC card into the reader.
+        (Note: this test will time-out after 20 seconds.)
+     2. Do not remove the device after this test.
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
+
+plugin: shell
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdhc-storage
+depends: mediacard/sdhc-insert
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-usb_read_write
+ {%- else %}
+     removable_storage_test.py -s 268400000 --memorycard sdio usb scsi --auto-reduce-size
+ {% endif -%}
+_summary: HW_SLOTSD_005 - Test reading & writing to a SDHC Card
+_description:
+ This test is automated and executes after the mediacard/sdhc-insert
+ test is run. It tests reading and writing to the SDHC card.
+
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdhc-remove
+depends: mediacard/sdhc-storage
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher removal mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard remove sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_006 - Test that removal of an SDHC card is detected
+_description:
+ PURPOSE:
+     This test will check that the system correctly detects
+     the removal of an SDHC card from the systems card reader.
+ STEPS:
+     1. Commence the test and then remove the SDHC card from the reader.
+        (Note: this test will time-out after 20 seconds.)
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
+
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdxc-insert
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher insertion mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard insert sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_007 - Test that insertion of an SDXC card is detected
+_description:
+ PURPOSE:
+     This test will check that the systems media card reader can
+     detect the insertion of a Secure Digital Extended Capacity (SDXC) media card
+ STEPS:
+     1. Commence the test and then insert an UNLOCKED SDXC card into the reader.
+        (Note: this test will time-out after 20 seconds.)
+     2. Do not remove the device after this test.
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
+
+plugin: shell
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdxc-storage
+depends: mediacard/sdxc-insert
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-usb_read_write
+ {%- else %}
+     removable_storage_test.py -s 268400000 --memorycard sdio usb scsi --auto-reduce-size
+ {% endif -%}
+_summary: HW_SLOTSD_008 - Test reading & writing to a SDXC Card
+_description:
+ This test is automated and executes after the mediacard/sdxc-insert
+ test is run. It tests reading and writing to the SDXC card.
+
+plugin: user-interact
+template-engine: jinja2
+category_id: SD
+id: mediacard/sdxc-remove
+depends: mediacard/sdxc-storage
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher removal mediacard
+ {%- else %}
+     removable_storage_watcher.py --memorycard remove sdio usb scsi --unmounted
+ {% endif -%}
+user: root
+_summary: HW_SLOTSD_009 - Test that removal of an SDXC card is detected
+_description:
+ PURPOSE:
+     This test will check that the system correctly detects
+     the removal of a SDXC card from the systems card reader.
+ STEPS:
+     1. Commence the test and then remove the SDXC card from the reader.
+        (Note: this test will time-out after 20 seconds.)
+ VERIFICATION:
+     The verification of this test is automated. Do not change the
+     automatically selected result.
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Micro-SD-slot/test-plan-SD.pxu b/org.oniroproject.integration-tests/units/Micro-SD-slot/test-plan-SD.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..0ecc3dc67972a3e7194c4426231b4c6a291addf2
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Micro-SD-slot/test-plan-SD.pxu
@@ -0,0 +1,14 @@
+id: mediacard-manual
+unit: test plan
+_name: SD Card
+_description: Manual mediacard tests for Snappy Ubuntu Core devices
+include:
+    mediacard/sd-insert
+    mediacard/sd-storage
+    mediacard/sd-remove
+    mediacard/sdhc-insert
+    mediacard/sdhc-storage
+    mediacard/sdhc-remove
+    mediacard/sdxc-insert
+    mediacard/sdxc-storage
+    mediacard/sdxc-remove
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Net-USB/category.pxu b/org.oniroproject.integration-tests/units/Net-USB/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..e8073cada817ec2cc43ed92ef512d63168857b4f
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-USB/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: NetworkUSB
+_name: NetworkUSB tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Net-USB/jobs-NetworkUSB.pxu b/org.oniroproject.integration-tests/units/Net-USB/jobs-NetworkUSB.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..d4dea1d272828c94bfa0383cd50c516efb759d74
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-USB/jobs-NetworkUSB.pxu
@@ -0,0 +1,34 @@
+unit: job
+id: usb/insert
+_summary: BENCH_NET_USB_001 - USB 2.0 storage device insertion detected
+_description:
+ Check system can detect USB 2.0 storage when inserted.
+ NOTE: Make sure the USB storage device has a partition before starting
+ the test.
+_steps:
+ 1. Press continue
+ 2. Wait until the message "INSERT NOW" is printed on the screen
+ 3. Connect USB 2.0 storage device
+_verification:
+ The verification of this test is automated.
+ Do not change the automatically selected result.
+plugin: user-interact
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-run_watcher insertion usb2
+ {%- else %}
+     removable_storage_watcher.py --unmounted insert usb
+ {% endif -%}
+category_id: NetworkUSB
+
+plugin: shell
+category_id: NetworkUSB
+id: usb/performance
+depends: usb/insert
+user: root
+command: removable_storage_test.py -s 268400000 -p 15 usb
+_summary: BENCH_NET_USB_002 - This test will check that your USB 2.0 port transfers data at a minimum expected speed
+_description:
+ This test will check that your USB 2.0 port transfers data at a
+ minimum expected speed.
diff --git a/org.oniroproject.integration-tests/units/Net-USB/test-plan-NetworkUSB.pxu b/org.oniroproject.integration-tests/units/Net-USB/test-plan-NetworkUSB.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..407a53300320ec521f04dc1a3c8966b5c90df172
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-USB/test-plan-NetworkUSB.pxu
@@ -0,0 +1,7 @@
+id: usb-performance
+unit: test plan
+_name: USB performance
+_description: Automated USB2 performance tests
+include:
+ usb/insert
+ usb/performance
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Net-WiFi/category.pxu b/org.oniroproject.integration-tests/units/Net-WiFi/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..9d469d60a6a4a339ad6e2be7dcf380f8ba47e2ea
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-WiFi/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Net-WiFi
+_name: Net-WiFi tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Net-WiFi/jobs-NetworkWifi.pxu b/org.oniroproject.integration-tests/units/Net-WiFi/jobs-NetworkWifi.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..a5cd45b4558d631f4a46a5a816d852b97d2dfd16
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-WiFi/jobs-NetworkWifi.pxu
@@ -0,0 +1,7 @@
+unit: job
+plugin: shell
+category_id: Net-WiFi
+id: Net-Wifi/wifi_time_to_reconnect
+command: wifi_time2reconnect.py
+_summary: BENCH_NET_WIFI_001 - Check the time needed to reconnect to a WIFI access point
+_description: Check the time needed to reconnect to a WIFI access point
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Net-WiFi/test-plan-NetworkWifi.pxu b/org.oniroproject.integration-tests/units/Net-WiFi/test-plan-NetworkWifi.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..8645ca3537608ca5c36a6a884493022f5dcc803c
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Net-WiFi/test-plan-NetworkWifi.pxu
@@ -0,0 +1,6 @@
+id: Network speed Wifi
+unit: test plan
+_name: Network speed Wifi
+_description: Check the time needed to reconnect to a WIFI access point
+include:
+ Net-Wifi/wifi_time_to_reconnect
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/SATA/category.pxu b/org.oniroproject.integration-tests/units/SATA/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..cc3f61ffe0372f04a4dcea0429fa434b1e3f20c0
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/SATA/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: eSATA
+_name: eSATA tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/SATA/jobs-SATA.pxu b/org.oniroproject.integration-tests/units/SATA/jobs-SATA.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..494e002fc413c56c421db391026fb0e4bc93d524
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/SATA/jobs-SATA.pxu
@@ -0,0 +1,44 @@
+unit: job
+plugin: user-interact
+id: esata/insert
+category_id: eSATA
+_summary: HW_SATA_001 - This test will check the system can detect the insertion of an eSATA HDD
+command: removable_storage_watcher.py insert ata_serial_esata
+_description:
+ PURPOSE:
+     This test will check the system can detect the insertion of an eSATA HDD
+ STEPS:
+     1. Click 'Test' to begin the test. This test will
+        timeout and fail if the insertion has not been detected within 20 seconds.
+     2. Plug an eSATA HDD into an available eSATA port.
+ VERIFICATION:
+     The verification of this test is automated. Do not change the automatically
+     selected result
+
+plugin: shell
+id: esata/storage-test
+_summary: HW_SATA_002 - This is an automated test which performs read/write operations on an attached eSATA HDD 
+category_id: eSATA
+user: root
+depends: esata/insert
+command: removable_storage_test.py -s 268400000 ata_serial_esata
+_description:
+ This is an automated test which performs read/write operations on an attached
+ eSATA HDD
+
+plugin: user-interact
+id: esata/remove
+_summary: HW_SATA_003 - This test will check the system can detect the removal of an eSATA HDD
+category_id: eSATA
+depends: esata/insert
+command: removable_storage_watcher.py remove ata_serial_esata
+_description:
+ PURPOSE:
+     This test will check the system can detect the removal of an eSATA HDD
+ STEPS:
+     1. Click 'Test' to begin the test. This test will timeout and fail if
+        the removal has not been detected within 20 seconds.
+     2. Remove the previously attached eSATA HDD from the eSATA port.
+ VERIFICATION:
+     The verification of this test is automated. Do not change the automatically
+     selected result
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/SATA/test-plan-SATA.pxu b/org.oniroproject.integration-tests/units/SATA/test-plan-SATA.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..0448b99ea3020b27ecbf4619a009e8a47e36d4f5
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/SATA/test-plan-SATA.pxu
@@ -0,0 +1,9 @@
+id: esata-cert-manual
+unit: test plan
+_name: eSATA (Manual)
+_description:
+ eSATA tests (Manual)
+include:
+ esata/insert      
+ esata/storage-test
+ esata/remove      
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Stress-test/category.pxu b/org.oniroproject.integration-tests/units/Stress-test/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..6317676c4e139d9f15f90ad99959202651544778
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Stress-test/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: StressTest
+_name: StressTest tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Stress-test/jobs-StressTest.pxu b/org.oniroproject.integration-tests/units/Stress-test/jobs-StressTest.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..ae1b99972f4fadbcc0c16339e3172bbf58fd23cd
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Stress-test/jobs-StressTest.pxu
@@ -0,0 +1,80 @@
+unit: job
+plugin: shell
+category_id: StressTest
+id: stress/reboot
+requires: executable.name == 'fwts'
+command:
+ rm -f "$PLAINBOX_SESSION_SHARE"/__result
+ pm_test.py --checkbox-respawn-cmd "$PLAINBOX_SESSION_SHARE"/__respawn_checkbox -r 100 --silent --log-level=notset reboot --log-dir="$PLAINBOX_SESSION_SHARE"
+user: root
+environ: PLAINBOX_SESSION_SHARE PM_TEST_DRY_RUN
+_summary: ROB_STRESS_REBOOT_001 - Stress reboot system (100 cycles)
+_description:
+ Stress reboot system (100 cycles)
+
+plugin: attachment
+category_id: StressTest
+id: stress/reboot_log
+_summary: ROB_STRESS_REBOOT_002 - Log for stress reboot test
+depends: stress/reboot
+command:
+ set -o pipefail
+ cat "$PLAINBOX_SESSION_SHARE"/*reboot.100.log
+
+plugin: shell
+category_id: StressTest
+id: stress/reboot_check
+_summary: ROB_STRESS_REBOOT_003 - Check logs for the stress reboot (100 cycles) test case
+depends: stress/reboot
+command: pm_log_check.py --log-level=notset "$PLAINBOX_SESSION_SHARE"/pm_test.reboot.100.log "$PLAINBOX_SESSION_SHARE"/pm_log_check_reboot.100.log
+_description: Check logs for the stress reboot (100 cycles) test case
+
+plugin: attachment
+category_id: StressTest
+id: stress/reboot_check_log
+_summary: ROB_STRESS_REBOOT_004 - Log for check logs test case (stress reboot 100 cycles)
+depends: stress/reboot_check
+command:
+ set -o pipefail
+ cat "$PLAINBOX_SESSION_SHARE"/pm_log_check_reboot.100.log
+
+plugin: shell
+category_id: StressTest
+id: stress/poweroff
+requires:
+ executable.name == 'fwts'
+ executable.name == 'x-terminal-emulator'
+command:
+ rm -f "$PLAINBOX_SESSION_SHARE"/__result
+ pm_test.py --checkbox-respawn-cmd "$PLAINBOX_SESSION_SHARE"/__respawn_checkbox -r 100 --silent --log-level=notset poweroff --log-dir="$PLAINBOX_SESSION_SHARE"
+user: root
+environ: PLAINBOX_SESSION_SHARE PM_TEST_DRY_RUN
+_summary: ROB_STRESS_POWEROFF_001 - Stress poweroff system (100 cycles)
+_description:
+ Stress poweroff system (100 cycles)
+
+plugin: attachment
+category_id: StressTest
+id: stress/poweroff_log
+_summary: ROB_STRESS_POWEROFF_002 - Log for stress poweroff test
+depends: stress/poweroff
+command:
+ set -o pipefail
+ cat "$PLAINBOX_SESSION_SHARE"/*poweroff.100.log
+
+plugin: shell
+category_id: StressTest
+id: stress/poweroff_check
+_summary: ROB_STRESS_POWEROFF_003 - Check logs for the stress poweroff (100 cycles) test case
+depends: stress/poweroff
+command: pm_log_check.py --log-level=notset "$PLAINBOX_SESSION_SHARE"/pm_test.poweroff.100.log "$PLAINBOX_SESSION_SHARE"/pm_log_check_poweroff.100.log
+_description: Check logs for the stress poweroff (100 cycles) test case
+
+plugin: attachment
+category_id: StressTest
+id: stress/poweroff_check_log
+_summary: ROB_STRESS_POWEROFF_004 - Log for check logs test case (stress poweroff 100 cycles)
+depends: stress/poweroff_check
+command:
+ set -o pipefail
+ cat "$PLAINBOX_SESSION_SHARE"/pm_log_check_poweroff.100.log
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Stress-test/packaging-StressTest.pxu b/org.oniroproject.integration-tests/units/Stress-test/packaging-StressTest.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..d33476aa18f509cfa32cdba46e51c511ebccd3ec
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Stress-test/packaging-StressTest.pxu
@@ -0,0 +1,4 @@
+# This is to install evemu-tools for the screen lock key event simulation
+unit: packaging meta-data
+os-id: debian
+Depends: evemu-tools
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Stress-test/test-plan-StressTest.pxu b/org.oniroproject.integration-tests/units/Stress-test/test-plan-StressTest.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..6f3d44c9e0fa4157cd4e999df58ce72169f87d0a
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Stress-test/test-plan-StressTest.pxu
@@ -0,0 +1,13 @@
+id: stress-100-reboot-poweroff-automated
+unit: test plan
+_name: Stress tests reboot and poweroff (Automated)
+_description: Power Management reboot and power off 100 cycles stress tests (automated)
+include:
+    stress/reboot        
+    stress/reboot_log
+    stress/reboot_check
+    stress/reboot_check_log
+    stress/poweroff      
+    stress/poweroff_log
+    stress/poweroff_check
+    stress/poweroff_check_log
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/TCP/category.pxu b/org.oniroproject.integration-tests/units/TCP/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..96b57371cff7cc934cbac62fd339d523d8fc4da3
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/TCP/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: TCP
+_name: TCP tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/TCP/jobs-TCP.pxu b/org.oniroproject.integration-tests/units/TCP/jobs-TCP.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..f72a16da349e1ea29a79faf081d9b9157a6da3a5
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/TCP/jobs-TCP.pxu
@@ -0,0 +1,21 @@
+unit: job
+id: ipv6_detect
+category_id: TCP
+_summary: SFL_NT_TCP/IP_IPV6_001 -  Test if the kernel is IPv6 ready
+command:
+  if test -f /proc/net/if_inet6; then
+    echo "Running kernel is IPv6 ready"
+    exit 0
+  fi
+  echo "/proc/net/if_inet6 not present"
+  echo "Running kernel does not appear to be IPv6 ready"
+  exit 1  
+
+
+plugin: shell
+depends: ipv6_detect
+id: ipv6_link_local_address_{interface}
+_summary: SFL_NT_TCP/IP_IPV6_002 -  Test that {interface} has an IPv6 link local address
+category_id: TCP
+command:
+  [ "$(ip -6 -o addr show dev {interface} scope link | wc -l)" -eq 1 ]
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/TCP/packaging-TCP.pxu b/org.oniroproject.integration-tests/units/TCP/packaging-TCP.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..73c64837f7ef9760083fde51460d66488d2b2fc2
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/TCP/packaging-TCP.pxu
@@ -0,0 +1,7 @@
+unit: packaging meta-data
+os-id: debian
+Depends: ntpdate, net-tools
+
+unit: packaging meta-data
+os-id: debian
+Depends: iperf3
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/TCP/test-plan-TCP.pxu b/org.oniroproject.integration-tests/units/TCP/test-plan-TCP.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..4911c74392bc3ed1344c5442ed87f70f639a1193
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/TCP/test-plan-TCP.pxu
@@ -0,0 +1,9 @@
+id: networking-automated
+unit: test plan
+_name: TCP/IP (Automated)
+_description: Automated networking tests for devices
+include:
+  ipv6_detect
+  ipv6_link_local_address_.*
+bootstrap_include:
+  device
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/USB/category.pxu b/org.oniroproject.integration-tests/units/USB/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..45a2518e291e1ddea225ea9d1ddd0d540ed1fedf
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/USB/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: USB
+_name: USB tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/USB/jobs-ControllerDetection.pxu b/org.oniroproject.integration-tests/units/USB/jobs-ControllerDetection.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..9555e9673969245930d237cb03fa1865df6bc879
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/USB/jobs-ControllerDetection.pxu
@@ -0,0 +1,53 @@
+unit: job
+id: usb/detect
+plugin: shell
+category_id: USB
+command:
+ set -o pipefail
+ if [[ -v SNAP ]]; then
+     checkbox-support-lsusb -f "$CHECKBOX_RUNTIME"/var/lib/usbutils/usb.ids 2>/dev/null | sed 's/.*\(ID .*\)/\1/' | head -n 4 || echo "No USB devices were detected" >&2
+ else
+     lsusb 2>/dev/null | sort || echo "No USB devices were detected" >&2
+ fi
+_summary: HW_USB_Controller_001 - Detects and shows USB devices attached to this system 
+_description: Detects and shows USB devices attached to this system.
+
+id: usb/storage-automated2
+category_id: USB
+template-engine: jinja2
+_summary: HW_USB_Controller_002 - Check system can read/write to USB 2.0 storage correctly
+_purpose:
+ Check system can read/write to USB 2.0 storage correctly
+_steps:
+ 1. This task is fully automatic and need USB 2.0 insertion test was applied first.
+_verification:
+ This task is fully automatic and will verify the result for you.
+plugin: shell
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-usb_read_write
+ {%- else %}
+     removable_storage_test.py -s 268400000 usb
+ {% endif -%}
+
+
+id: usb/storage-automated3
+category_id: USB
+template-engine: jinja2
+_summary: HW_USB_Controller_003 - Check system can read/write to USB 3.0 storage correctly
+_purpose:
+ Check system can read/write to USB 3.0 storage devices correctly
+_steps:
+ 1. This task is fully automatic and need USB 3.0 insertion test was applied first.
+_verification:
+ This task is fully automatic and will verify the result for you.
+plugin: shell
+user: root
+command:
+ {%- if __on_ubuntucore__ %}
+     checkbox-support-usb_read_write
+ {%- else %}
+     removable_storage_test.py -s 268400000 -m 500000000 usb --driver xhci_hcd
+ {% endif -%}
+
diff --git a/org.oniroproject.integration-tests/units/USB/jobs-Peripheral.pxu b/org.oniroproject.integration-tests/units/USB/jobs-Peripheral.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..da2bd5539eae6e3c9a9b1937ba1321d5628c9d31
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/USB/jobs-Peripheral.pxu
@@ -0,0 +1,28 @@
+unit: job
+id: usb/keyboard
+plugin: user-interact-verify
+category_id: USB
+command: keyboard_test.py
+_summary: HW_USB_Keyboard_001 - Check USB input device works
+_description:
+ PURPOSE:
+     This test will check that you can use a USB HID device
+ STEPS:
+     1. Enable either a USB mouse or keyboard
+     2. For keyboards, commence the test to launch a small tool. Type some text and close the tool.
+ VERIFICATION:
+     Did the device work as expected?
+
+id: usb/mouse
+plugin: user-interact-verify
+category_id: USB
+command: keyboard_test.py
+_summary: HW_USB_Mouse_002 - Check USB input device works
+_description:
+ PURPOSE:
+     This test will check that you can use a USB HID device
+ STEPS:
+     1. Enable either a USB mouse or keyboard
+     2. For mice, perform actions such as moving the pointer, right and left button clicks and double clicks
+ VERIFICATION:
+     Did the device work as expected?
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/USB/test-plan-ControllerDetection.pxu b/org.oniroproject.integration-tests/units/USB/test-plan-ControllerDetection.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..05df1ab3af5fa593225a88f9476a69559f658b7f
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/USB/test-plan-ControllerDetection.pxu
@@ -0,0 +1,9 @@
+id: usb-cert-manual
+unit: test plan
+_name: USB Controller Detection (Manual)
+_description:
+ USB tests (Manual)
+include:
+ usb/detect
+ usb/storage-automated2
+ usb/storage-automated3
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/USB/test-plan-Peripheral.pxu b/org.oniroproject.integration-tests/units/USB/test-plan-Peripheral.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..5c28ebc4fcbd692e68d4686e265099a4054e9c3e
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/USB/test-plan-Peripheral.pxu
@@ -0,0 +1,8 @@
+id: usb-cert-manual-peripherals
+unit: test plan
+_name: USB Peripherals keyboard/mouse (Manual)
+_description:
+ USB tests peripherals (Manual)
+include:
+ usb/keyboard
+ usb/mouse
diff --git a/org.oniroproject.integration-tests/units/Video/category.pxu b/org.oniroproject.integration-tests/units/Video/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..9871ea0b350ce5e08eae98250d630891bedc0029
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Video/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Video
+_name: Video tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Video/jobs-Video.pxu b/org.oniroproject.integration-tests/units/Video/jobs-Video.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..fcd2e9a56b941035340ad6a072acbed586c0f9d9
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Video/jobs-Video.pxu
@@ -0,0 +1,37 @@
+unit: job
+id: monitor/hdmi
+_summary: HW_VIDEO_HDMI_001 - Monitor works (HDMI)
+_purpose:
+ Check output to display through HDMI port
+_steps:
+ 1. Connect display to HDMI port
+ 2. Check the display
+_verification:
+ Output to display works
+plugin: manual
+category_id: Video
+
+id: monitor/displayport
+_summary: HW_VIDEO_DPORT_001 - Monitor works (DisplayPort)
+_purpose:
+ Check output to display through DisplayPort
+_steps:
+ 1. Connect display to DisplayPort
+ 2. Check the display
+_verification:
+ Output to display works
+plugin: manual
+category_id: Video
+
+id: monitor/displayport_hotplug
+_summary: HW_VIDEO_DPORT_002 - Can hotplug monitor (DisplayPort)
+plugin: manual
+category_id: Video
+_purpose:
+     This test will check the DisplayPort port and the ability to do hotplugging.
+_steps:
+     Skip this test if your system does not have a DisplayPort port.
+     1. If a display is already connected, unplug it.
+     2. (Re-)Connect a display to the DisplayPort port on your system
+_verification:
+     Was the interface displayed correctly on the screen?
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Video/test-plan-Video.pxu b/org.oniroproject.integration-tests/units/Video/test-plan-Video.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..2ab10e4507d3adc1084b548eb7c5a078603615e0
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Video/test-plan-Video.pxu
@@ -0,0 +1,14 @@
+id: monitor-manual-HDMI
+unit: test plan
+_name: Video HDMI
+_description: Manual monitor tests for Snappy Ubuntu Core devices
+include:
+    monitor/hdmi
+
+id: monitor-manual-displayport
+unit: test plan
+_name: Video Displayport
+_description: Manual monitor tests for Snappy Ubuntu Core devices
+include:
+    monitor/displayport
+    monitor/displayport_hotplug
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Wifi/category.pxu b/org.oniroproject.integration-tests/units/Wifi/category.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..8b8a44b06f265e5ff2aa64870b91197b37c97810
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Wifi/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: Wifi
+_name: Wifi tests
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Wifi/jobs-Wifi.pxu b/org.oniroproject.integration-tests/units/Wifi/jobs-Wifi.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..ed49d1bc8c41e98197555d1c12fc3fe73689f42c
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Wifi/jobs-Wifi.pxu
@@ -0,0 +1,114 @@
+unit: job
+id: wireless/detect
+category_id: Wifi
+plugin: shell
+_summary: HW_WIFI_001 - Detect if at least one Wireless LAN device is detected
+command:
+  network_device_info.py detect WIRELESS
+
+
+plugin: shell
+id: wireless/wireless_scanning_{{ interface }}
+_summary: HW_WIFI_002 - Test system can discover Wi-Fi networks on {{ interface }}
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py scan {{ interface }}
+category_id: Wifi
+_description:
+ Check system can find a wireless network AP nearby
+
+
+plugin: shell
+id: wireless/wireless_connection_open_ax_nm_{{ interface }}
+_summary: HW_WIFI_003 - Check system can connect to insecure 802.11ax AP
+_description:
+  Check system can connect to insecure 802.11ax AP
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py open {{ interface }} "$OPEN_AX_SSID"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_open_ac_nm_{{ interface }}
+_summary: HW_WIFI_004 - Check system can connect to insecure 802.11ac AP
+_description:
+  Check system can connect to insecure 802.11ac AP
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py open {{ interface }} "$OPEN_AC_SSID"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_open_bg_nm_{{ interface }}
+_summary: HW_WIFI_005 - Check system can connect to insecure 802.11b/g AP
+_description:
+  Check system can connect to insecure 802.11b/g AP
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py open {{ interface }} "$OPEN_BG_SSID"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_open_n_nm_{{ interface }}
+_summary: HW_WIFI_006 - Check system can connect to insecure 802.11n AP
+_description:
+  Check system can connect to insecure 802.11n AP
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py open {{ interface }} "$OPEN_N_SSID"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_wpa_ax_nm_{{ interface }}
+_summary: HW_WIFI_007 - Check system can connect to 802.11ax AP with wpa security
+_description:
+  Check system can connect to 802.11ax AP with wpa security
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py secured {{ interface }} "$WPA_AX_SSID" "$WPA_AX_PSK"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_wpa_ac_nm_{{ interface }}
+_summary: HW_WIFI_008 - Check system can connect to 802.11ac AP with wpa security
+_description:
+  Check system can connect to 802.11ac AP with wpa security
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py secured {{ interface }} "$WPA_AC_SSID" "$WPA_AC_PSK"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_wpa_bg_nm_{{ interface }}
+_summary: HW_WIFI_009 - Check system can connect to 802.11b/g AP with wpa security
+_description:
+  Check system can connect to 802.11b/g AP with wpa security
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py secured {{ interface }} "$WPA_BG_SSID" "$WPA_BG_PSK"
+category_id: Wifi
+
+
+plugin: shell
+id: wireless/wireless_connection_wpa_n_nm_{{ interface }}
+_summary: HW_WIFI_010 - Check system can connect to 802.11n AP with wpa security
+_description:
+  Check system can connect to 802.11n AP with wpa security
+user: root
+command:
+  net_driver_info.py "$NET_DRIVER_INFO"
+  wifi_nmcli_test.py secured {{ interface }} "$WPA_N_SSID" "$WPA_N_PSK"
+category_id: Wifi
\ No newline at end of file
diff --git a/org.oniroproject.integration-tests/units/Wifi/test-plan-Wifi.pxu b/org.oniroproject.integration-tests/units/Wifi/test-plan-Wifi.pxu
new file mode 100644
index 0000000000000000000000000000000000000000..d1747196f2678439dbc318c14d121b258b7cc04d
--- /dev/null
+++ b/org.oniroproject.integration-tests/units/Wifi/test-plan-Wifi.pxu
@@ -0,0 +1,19 @@
+id: wireless-automated
+unit: test plan
+_name: Wireless (Automated)
+_description:
+ Automated connection tests for unencrypted or WPA-encrypted 802.11 bg, n, ac, ax
+ networks.
+include:
+    wireless/detect
+    wireless/wireless_scanning_.*
+    wireless/wireless_connection_open_ax_nm_.*
+    wireless/wireless_connection_open_ac_nm_.*
+    wireless/wireless_connection_open_bg_nm_.*
+    wireless/wireless_connection_open_n_nm_.*
+    wireless/wireless_connection_wpa_ax_nm_.*
+    wireless/wireless_connection_wpa_ac_nm_.*
+    wireless/wireless_connection_wpa_bg_nm_.*
+    wireless/wireless_connection_wpa_n_nm_.*
+bootstrap_include:
+    device
\ No newline at end of file