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