diff options
Diffstat (limited to 'yocto-poky/bitbake/lib/toaster/contrib')
24 files changed, 3557 insertions, 0 deletions
diff --git a/yocto-poky/bitbake/lib/toaster/contrib/README b/yocto-poky/bitbake/lib/toaster/contrib/README new file mode 100644 index 000000000..46d0ff008 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/README @@ -0,0 +1,6 @@ +contrib directory for toaster + +This directory holds code that works with Toaster, without being an integral part of the Toaster project. +It is intended for testing code, testing fixtures, tools for Toaster, etc. + +NOTE: This directory is NOT a Python module. diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore new file mode 100644 index 000000000..c45652d29 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore @@ -0,0 +1,10 @@ +*.pyc +*.swp +*.swo +*.kpf +*.egg-info/ +.idea +.tox +tmp/ +dist/ +.DS_Store diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml new file mode 100644 index 000000000..a920f3945 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml @@ -0,0 +1,50 @@ +language: python +python: + - "2.7" + - "3.4" +services: + - mysql + - postgresql +env: + - DJANGO=1.4 DB=sqlite + - DJANGO=1.4 DB=mysql + - DJANGO=1.4 DB=postgres + - DJANGO=1.5 DB=sqlite + - DJANGO=1.5 DB=mysql + - DJANGO=1.5 DB=postgres + - DJANGO=1.6 DB=sqlite + - DJANGO=1.6 DB=mysql + - DJANGO=1.6 DB=postgres + - DJANGO=1.7 DB=sqlite + - DJANGO=1.7 DB=mysql + - DJANGO=1.7 DB=postgres + +matrix: + exclude: + - python: "3.4" + env: DJANGO=1.4 DB=sqlite + - python: "3.4" + env: DJANGO=1.4 DB=mysql + - python: "3.4" + env: DJANGO=1.4 DB=postgres + - python: "3.4" + env: DJANGO=1.5 DB=sqlite + - python: "3.4" + env: DJANGO=1.5 DB=mysql + - python: "3.4" + env: DJANGO=1.5 DB=postgres + - python: "3.4" + env: DJANGO=1.6 DB=mysql + - python: "3.4" + env: DJANGO=1.7 DB=mysql + +before_script: + - mysql -e 'create database aggregation;' + - psql -c 'create database aggregation;' -U postgres +install: + - pip install six + - if [ "$DB" == "mysql" ]; then pip install mysql-python; fi + - if [ "$DB" == "postgres" ]; then pip install psycopg2; fi + - pip install -q Django==$DJANGO --use-mirrors +script: + - ./runtests.py --settings=tests.test_$DB diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE new file mode 100644 index 000000000..6b7c3b174 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2012 Henrique Bastos + +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. diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst new file mode 100644 index 000000000..739d4dacc --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst @@ -0,0 +1,156 @@ +Django Aggregate If: Condition aggregates for Django +==================================================== + +.. image:: https://travis-ci.org/henriquebastos/django-aggregate-if.png?branch=master + :target: https://travis-ci.org/henriquebastos/django-aggregate-if + :alt: Test Status + +.. image:: https://landscape.io/github/henriquebastos/django-aggregate-if/master/landscape.png + :target: https://landscape.io/github/henriquebastos/django-aggregate-if/master + :alt: Code Helth + +.. image:: https://pypip.in/v/django-aggregate-if/badge.png + :target: https://crate.io/packages/django-aggregate-if/ + :alt: Latest PyPI version + +.. image:: https://pypip.in/d/django-aggregate-if/badge.png + :target: https://crate.io/packages/django-aggregate-if/ + :alt: Number of PyPI downloads + +*Aggregate-if* adds conditional aggregates to Django. + +Conditional aggregates can help you reduce the ammount of queries to obtain +aggregated information, like statistics for example. + +Imagine you have a model ``Offer`` like this one: + +.. code-block:: python + + class Offer(models.Model): + sponsor = models.ForeignKey(User) + price = models.DecimalField(max_digits=9, decimal_places=2) + status = models.CharField(max_length=30) + expire_at = models.DateField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + OPEN = "OPEN" + REVOKED = "REVOKED" + PAID = "PAID" + +Let's say you want to know: + +#. How many offers exists in total; +#. How many of them are OPEN, REVOKED or PAID; +#. How much money was offered in total; +#. How much money is in OPEN, REVOKED and PAID offers; + +To get these informations, you could query: + +.. code-block:: python + + from django.db.models import Count, Sum + + Offer.objects.count() + Offer.objects.filter(status=Offer.OPEN).aggregate(Count('pk')) + Offer.objects.filter(status=Offer.REVOKED).aggregate(Count('pk')) + Offer.objects.filter(status=Offer.PAID).aggregate(Count('pk')) + Offer.objects.aggregate(Sum('price')) + Offer.objects.filter(status=Offer.OPEN).aggregate(Sum('price')) + Offer.objects.filter(status=Offer.REVOKED).aggregate(Sum('price')) + Offer.objects.filter(status=Offer.PAID).aggregate(Sum('price')) + +In this case, **8 queries** were needed to retrieve the desired information. + +With conditional aggregates you can get it all with only **1 query**: + +.. code-block:: python + + from django.db.models import Q + from aggregate_if import Count, Sum + + Offer.objects.aggregate( + pk__count=Count('pk'), + pk__open__count=Count('pk', only=Q(status=Offer.OPEN)), + pk__revoked__count=Count('pk', only=Q(status=Offer.REVOKED)), + pk__paid__count=Count('pk', only=Q(status=Offer.PAID)), + pk__sum=Sum('price'), + pk__open__sum=Sum('price', only=Q(status=Offer.OPEN)), + pk__revoked__sum=Sum('price'), only=Q(status=Offer.REVOKED)), + pk__paid__sum=Sum('price'), only=Q(status=Offer.PAID)) + ) + +Installation +------------ + +*Aggregate-if* works with Django 1.4, 1.5, 1.6 and 1.7. + +To install it, simply: + +.. code-block:: bash + + $ pip install django-aggregate-if + +Inspiration +----------- + +There is a 5 years old `ticket 11305`_ that will (*hopefully*) implement this feature into +Django 1.8. + +Using Django 1.6, I still wanted to avoid creating custom queries for very simple +conditional aggregations. So I've cherry picked those ideas and others from the +internet and built this library. + +This library uses the same API and tests proposed on `ticket 11305`_, so when the +new feature is available you can easily replace ``django-aggregate-if``. + +Limitations +----------- + +Conditions involving joins with aliases are not supported yet. If you want to +help adding this feature, you're welcome to check the `first issue`_. + +Contributors +------------ + +* `Henrique Bastos <http://github.com/henriquebastos>`_ +* `Iuri de Silvio <https://github.com/iurisilvio>`_ +* `Hampus Stjernhav <https://github.com/champ>`_ +* `Bradley Martsberger <https://github.com/martsberger>`_ +* `Markus Bertheau <https://github.com/mbertheau>`_ +* `end0 <https://github.com/end0>`_ +* `Scott Sexton <https://github.com/scottsexton>`_ +* `Mauler <https://github.com/mauler>`_ +* `trbs <https://github.com/trbs>`_ + +Changelog +--------- + +0.5 + - Support for Django 1.7 + +0.4 + - Use tox to run tests. + - Add support for Django 1.6. + - Add support for Python3. + - The ``only`` parameter now freely supports joins independent of the main query. + - Adds support for alias relabeling permitting excludes and updates with aggregates filtered on remote foreign key relations. + +0.3.1 + - Fix quotation escaping. + - Fix boolean casts on Postgres. + +0.2 + - Fix postgres issue with LIKE conditions. + +0.1 + - Initial release. + + +License +======= + +The MIT License. + +.. _ticket 11305: https://code.djangoproject.com/ticket/11305 +.. _first issue: https://github.com/henriquebastos/django-aggregate-if/issues/1 diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py new file mode 100644 index 000000000..d5f342717 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py @@ -0,0 +1,164 @@ +# coding: utf-8 +''' +Implements conditional aggregates. + +This code was based on the work of others found on the internet: + +1. http://web.archive.org/web/20101115170804/http://www.voteruniverse.com/Members/jlantz/blog/conditional-aggregates-in-django +2. https://code.djangoproject.com/ticket/11305 +3. https://groups.google.com/forum/?fromgroups=#!topic/django-users/cjzloTUwmS0 +4. https://groups.google.com/forum/?fromgroups=#!topic/django-users/vVprMpsAnPo +''' +from __future__ import unicode_literals +from django.utils import six +import django +from django.db.models.aggregates import Aggregate as DjangoAggregate +from django.db.models.sql.aggregates import Aggregate as DjangoSqlAggregate + + +VERSION = django.VERSION[:2] + + +class SqlAggregate(DjangoSqlAggregate): + conditional_template = '%(function)s(CASE WHEN %(condition)s THEN %(field)s ELSE null END)' + + def __init__(self, col, source=None, is_summary=False, condition=None, **extra): + super(SqlAggregate, self).__init__(col, source, is_summary, **extra) + self.condition = condition + + def relabel_aliases(self, change_map): + if VERSION < (1, 7): + super(SqlAggregate, self).relabel_aliases(change_map) + if self.has_condition: + condition_change_map = dict((k, v) for k, v in \ + change_map.items() if k in self.condition.query.alias_map + ) + self.condition.query.change_aliases(condition_change_map) + + def relabeled_clone(self, change_map): + self.relabel_aliases(change_map) + return super(SqlAggregate, self).relabeled_clone(change_map) + + def as_sql(self, qn, connection): + if self.has_condition: + self.sql_template = self.conditional_template + self.extra['condition'] = self._condition_as_sql(qn, connection) + + return super(SqlAggregate, self).as_sql(qn, connection) + + @property + def has_condition(self): + # Warning: bool(QuerySet) will hit the database + return self.condition is not None + + def _condition_as_sql(self, qn, connection): + ''' + Return sql for condition. + ''' + def escape(value): + if isinstance(value, bool): + value = str(int(value)) + if isinstance(value, six.string_types): + # Escape params used with LIKE + if '%' in value: + value = value.replace('%', '%%') + # Escape single quotes + if "'" in value: + value = value.replace("'", "''") + # Add single quote to text values + value = "'" + value + "'" + return value + + sql, param = self.condition.query.where.as_sql(qn, connection) + param = map(escape, param) + + return sql % tuple(param) + + +class SqlSum(SqlAggregate): + sql_function = 'SUM' + + +class SqlCount(SqlAggregate): + is_ordinal = True + sql_function = 'COUNT' + sql_template = '%(function)s(%(distinct)s%(field)s)' + conditional_template = '%(function)s(%(distinct)sCASE WHEN %(condition)s THEN %(field)s ELSE null END)' + + def __init__(self, col, distinct=False, **extra): + super(SqlCount, self).__init__(col, distinct=distinct and 'DISTINCT ' or '', **extra) + + +class SqlAvg(SqlAggregate): + is_computed = True + sql_function = 'AVG' + + +class SqlMax(SqlAggregate): + sql_function = 'MAX' + + +class SqlMin(SqlAggregate): + sql_function = 'MIN' + + +class Aggregate(DjangoAggregate): + def __init__(self, lookup, only=None, **extra): + super(Aggregate, self).__init__(lookup, **extra) + self.only = only + self.condition = None + + def _get_fields_from_Q(self, q): + fields = [] + for child in q.children: + if hasattr(child, 'children'): + fields.extend(self._get_fields_from_Q(child)) + else: + fields.append(child) + return fields + + def add_to_query(self, query, alias, col, source, is_summary): + if self.only: + self.condition = query.model._default_manager.filter(self.only) + for child in self._get_fields_from_Q(self.only): + field_list = child[0].split('__') + # Pop off the last field if it's a query term ('gte', 'contains', 'isnull', etc.) + if field_list[-1] in query.query_terms: + field_list.pop() + # setup_joins have different returns in Django 1.5 and 1.6, but the order of what we need remains. + result = query.setup_joins(field_list, query.model._meta, query.get_initial_alias(), None) + join_list = result[3] + + fname = 'promote_alias_chain' if VERSION < (1, 5) else 'promote_joins' + args = (join_list, True) if VERSION < (1, 7) else (join_list,) + + promote = getattr(query, fname) + promote(*args) + + aggregate = self.sql_klass(col, source=source, is_summary=is_summary, condition=self.condition, **self.extra) + query.aggregates[alias] = aggregate + + +class Sum(Aggregate): + name = 'Sum' + sql_klass = SqlSum + + +class Count(Aggregate): + name = 'Count' + sql_klass = SqlCount + + +class Avg(Aggregate): + name = 'Avg' + sql_klass = SqlAvg + + +class Max(Aggregate): + name = 'Max' + sql_klass = SqlMax + + +class Min(Aggregate): + name = 'Min' + sql_klass = SqlMin diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py new file mode 100755 index 000000000..2e55864e3 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import os +import sys +from optparse import OptionParser + + +def parse_args(): + parser = OptionParser() + parser.add_option('-s', '--settings', help='Define settings.') + parser.add_option('-t', '--unittest', help='Define which test to run. Default all.') + options, args = parser.parse_args() + + if not options.settings: + parser.print_help() + sys.exit(1) + + if not options.unittest: + options.unittest = ['aggregation'] + + return options + + +def get_runner(settings_module): + ''' + Asks Django for the TestRunner defined in settings or the default one. + ''' + os.environ['DJANGO_SETTINGS_MODULE'] = settings_module + + import django + from django.test.utils import get_runner + from django.conf import settings + + if hasattr(django, 'setup'): + django.setup() + + return get_runner(settings) + + +def runtests(): + options = parse_args() + TestRunner = get_runner(options.settings) + runner = TestRunner(verbosity=1, interactive=True, failfast=False) + sys.exit(runner.run_tests([])) + + +if __name__ == '__main__': + runtests() diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py new file mode 100644 index 000000000..aed3db14d --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py @@ -0,0 +1,33 @@ +# coding: utf-8 +from setuptools import setup +import os + + +setup(name='django-aggregate-if', + version='0.5', + description='Conditional aggregates for Django, just like the famous SumIf in Excel.', + long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(), + author="Henrique Bastos", author_email="henrique@bastos.net", + license="MIT", + py_modules=['aggregate_if'], + install_requires=[ + 'six>=1.6.1', + ], + zip_safe=False, + platforms='any', + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Topic :: Database', + 'Topic :: Software Development :: Libraries', + ], + url='http://github.com/henriquebastos/django-aggregate-if/', +) diff --git a/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini new file mode 100644 index 000000000..78beb148a --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini @@ -0,0 +1,198 @@ +[tox] +envlist = + py27-django1.4-sqlite, + py27-django1.4-postgres, + py27-django1.4-mysql, + + py27-django1.5-sqlite, + py27-django1.5-postgres, + py27-django1.5-mysql, + + py27-django1.6-sqlite, + py27-django1.6-postgres, + py27-django1.6-mysql, + + py27-django1.7-sqlite, + py27-django1.7-postgres, + py27-django1.7-mysql, + + py34-django1.6-sqlite, + py34-django1.6-postgres, + #py34-django1.6-mysql + + py34-django1.7-sqlite, + py34-django1.7-postgres, + #py34-django1.7-mysql + +[testenv] +whitelist_externals= + mysql + psql + +# Python 2.7 +# Django 1.4 +[testenv:py27-django1.4-sqlite] +basepython = python2.7 +deps = + django==1.4 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py27-django1.4-postgres] +basepython = python2.7 +deps = + django==1.4 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py27-django1.4-mysql] +basepython = python2.7 +deps = + django==1.4 + mysql-python +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' + +# Django 1.5 +[testenv:py27-django1.5-sqlite] +basepython = python2.7 +deps = + django==1.5 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py27-django1.5-postgres] +basepython = python2.7 +deps = + django==1.5 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py27-django1.5-mysql] +basepython = python2.7 +deps = + django==1.5 + mysql-python +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' + +# Django 1.6 +[testenv:py27-django1.6-sqlite] +basepython = python2.7 +deps = + django==1.6 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py27-django1.6-postgres] +basepython = python2.7 +deps = + django==1.6 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py27-django1.6-mysql] +basepython = python2.7 +deps = + django==1.6 + mysql-python +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' + + +# Python 2.7 and Django 1.7 +[testenv:py27-django1.7-sqlite] +basepython = python2.7 +deps = + django==1.7 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py27-django1.7-postgres] +basepython = python2.7 +deps = + django==1.7 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py27-django1.7-mysql] +basepython = python2.7 +deps = + django==1.7 + mysql-python +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' + + +# Python 3.4 +# Django 1.6 +[testenv:py34-django1.6-sqlite] +basepython = python3.4 +deps = + django==1.6 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py34-django1.6-postgres] +basepython = python3.4 +deps = + django==1.6 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py34-django1.6-mysql] +basepython = python3.4 +deps = + django==1.6 + mysql-python3 +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' + + +# Python 3.4 +# Django 1.7 +[testenv:py34-django1.7-sqlite] +basepython = python3.4 +deps = + django==1.7 +commands = python runtests.py --settings tests.test_sqlite + +[testenv:py34-django1.7-postgres] +basepython = python3.4 +deps = + django==1.7 + psycopg2 +commands = + psql -c 'create database aggregation;' postgres + python runtests.py --settings tests.test_postgres + psql -c 'drop database aggregation;' postgres + +[testenv:py34-django1.7-mysql] +basepython = python3.4 +deps = + django==1.7 + mysql-python3 +commands = + mysql -e 'create database aggregation;' + python runtests.py --settings tests.test_mysql + mysql -e 'drop database aggregation;' diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/README b/yocto-poky/bitbake/lib/toaster/contrib/tts/README new file mode 100644 index 000000000..22fa5673b --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/README @@ -0,0 +1,41 @@ + +Toaster Testing Framework +Yocto Project + + +Rationale +------------ +As Toaster contributions grow with the number of people that contribute code, verifying each patch prior to submitting upstream becomes a hard-to-scale problem for humans. We devised this system in order to run patch-level validation, trying to eliminate common problems from submitted patches, in an automated fashion. + +The Toaster Testing Framework is a set of Python scripts that provides an extensible way to write smoke and regression tests that will be run on each patch set sent for review on the toaster mailing list. + + +Usage +------------ +There are three main executable scripts in this directory. + * runner.py is designed to be run from the command line. It requires, as mandatory parameter, a branch name on poky-contrib, branch which contains the patches to be tested. The program will auto-discover the available tests residing in this directory by looking for unittest classes, and will run the tests on the branch dumping the output to the standard output. Optionally, it can take parameters inhibiting the branch checkout, or specifying a single test to be run, for debugging purposes. + * launcher.py is a designed to be run from a crontab or similar scheduling mechanism. It looks up a backlog file containing branches-to-test (named tasks in the source code), select the first one in FIFO manner, and launch runner.py on it. It will await for completion, and email the standard output and standard error dumps from the runner.py execution + * recv.py is an email receiver, designed to be called as a pipe from a .forward file. It is used to monitor a mailing list, for example, and add tasks to the backlog based on review requests coming on the mailing list. + + +Installation +------------ +As prerequisite, we expect a functioning email system on a machine with Python2. + +The broad steps to installation +* set up the .forward on the receiving email account to pipe to the recv.py file +* edit config.py and settings.json to alter for local installation settings +* on email receive, verify backlog.txt to see that the tasks are received and marked for processing +* execute launcher.py in command line to verify that a test occurs with no problems, and that the outgoing email is delivered +* add launcher.py + + + +Contribute +------------ +What we need are tests. Add your own tests to either tests.py file, or to a new file. +Use "config.logger" to write logs that will make it to email. + +Commonly used code should be going to shellutils, and configuration to config.py. + +Contribute code by emailing patches to the list: toaster@yoctoproject.org (membership required) diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/TODO b/yocto-poky/bitbake/lib/toaster/contrib/tts/TODO new file mode 100644 index 000000000..117192106 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/TODO @@ -0,0 +1,9 @@ +We need to implement tests: + +automated link checker; currently +$ linkchecker -t 1000 -F csv http://localhost:8000/ + +integrate the w3c-validation service; currently +$ python urlcheck.py + + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/config.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/config.py new file mode 100644 index 000000000..40d45f3b7 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/config.py @@ -0,0 +1,98 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + +# This is the configuration/single module for tts +# everything that would be a global variable goes here + +import os, sys, logging +import socket + +LOGDIR = "log" +SETTINGS_FILE = os.path.join(os.path.dirname(__file__), "settings.json") +TEST_DIR_NAME = "tts_testdir" + +DEBUG = True + +OWN_PID = os.getpid() + +W3C_VALIDATOR = "http://icarus.local/w3c-validator/check?doctype=HTML5&uri=" + +TOASTER_PORT = 56789 + +TESTDIR = None + +#we parse the w3c URL to know where to connect + +import urlparse + +def get_public_ip(): + temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + parsed_url = urlparse.urlparse("http://icarus.local/w3c-validator/check?doctype=HTML5&uri=") + temp_socket.connect((parsed_url.netloc, 80 if parsed_url.port is None else parsed_url.port)) + public_ip = temp_socket.getsockname()[0] + temp_socket.close() + return public_ip + +TOASTER_BASEURL = "http://%s:%d/" % (get_public_ip(), TOASTER_PORT) + + +OWN_EMAIL_ADDRESS = "Toaster Testing Framework <alexandru.damian@intel.com>" +REPORT_EMAIL_ADDRESS = "alexandru.damian@intel.com" + +# make sure we have the basic logging infrastructure + +#pylint: disable=invalid-name +# we disable the invalid name because the module-level "logger" is used througout bitbake +logger = logging.getLogger("toastertest") +__console__ = logging.StreamHandler(sys.stdout) +__console__.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s")) +logger.addHandler(__console__) +logger.setLevel(logging.DEBUG) + + +# singleton file names +LOCKFILE = "/tmp/ttf.lock" +BACKLOGFILE = os.path.join(os.path.dirname(__file__), "backlog.txt") + +# task states +def enum(*sequential, **named): + enums = dict(zip(sequential, range(len(sequential))), **named) + reverse = dict((value, key) for key, value in enums.iteritems()) + enums['reverse_mapping'] = reverse + return type('Enum', (), enums) + + +class TASKS(object): + #pylint: disable=too-few-public-methods + PENDING = "PENDING" + INPROGRESS = "INPROGRESS" + DONE = "DONE" + + @staticmethod + def next_task(task): + if task == TASKS.PENDING: + return TASKS.INPROGRESS + if task == TASKS.INPROGRESS: + return TASKS.DONE + raise Exception("Invalid next task state for %s" % task) + +# TTS specific +CONTRIB_REPO = "git@git.yoctoproject.org:poky-contrib" + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/launcher.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/launcher.py new file mode 100755 index 000000000..e5794c1c5 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/launcher.py @@ -0,0 +1,101 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + +# Program to run the next task listed from the backlog.txt; designed to be +# run from crontab. + +from __future__ import print_function +import sys, os, config, shellutils +from shellutils import ShellCmdException + +# Import smtplib for the actual sending function +import smtplib + +# Import the email modules we'll need +from email.mime.text import MIMEText + +def _take_lockfile(): + return shellutils.lockfile(shellutils.mk_lock_filename()) + + +def read_next_task_by_state(task_state, task_name=None): + if not os.path.exists(os.path.join(os.path.dirname(__file__), config.BACKLOGFILE)): + return None + os.rename(config.BACKLOGFILE, config.BACKLOGFILE + ".tmp") + task = None + with open(config.BACKLOGFILE + ".tmp", "r") as f_in: + with open(config.BACKLOGFILE, "w") as f_out: + for line in f_in.readlines(): + if task is None: + fields = line.strip().split("|", 2) + if fields[1] == task_state: + if task_name is None or task_name == fields[0]: + task = fields[0] + print("Updating %s %s to %s" % (task, task_state, config.TASKS.next_task(task_state))) + line = "%s|%s\n" % (task, config.TASKS.next_task(task_state)) + f_out.write(line) + os.remove(config.BACKLOGFILE + ".tmp") + return task + +def send_report(task_name, plaintext, errtext=None): + if errtext is None: + msg = MIMEText(plaintext) + else: + if plaintext is None: + plaintext = "" + msg = MIMEText("--STDOUT dump--\n\n%s\n\n--STDERR dump--\n\n%s" % (plaintext, errtext)) + + msg['Subject'] = "[review-request] %s - smoke test results" % task_name + msg['From'] = config.OWN_EMAIL_ADDRESS + msg['To'] = config.REPORT_EMAIL_ADDRESS + + smtp_connection = smtplib.SMTP("localhost") + smtp_connection.sendmail(config.OWN_EMAIL_ADDRESS, [config.REPORT_EMAIL_ADDRESS], msg.as_string()) + smtp_connection.quit() + +def main(): + # we don't do anything if we have another instance of us running + lock_file = _take_lockfile() + + if lock_file is None: + if config.DEBUG: + print("Concurrent script in progress, exiting") + sys.exit(1) + + next_task = read_next_task_by_state(config.TASKS.PENDING) + if next_task is not None: + print("Next task is", next_task) + errtext = None + out = None + try: + out = shellutils.run_shell_cmd("%s %s" % (os.path.join(os.path.dirname(__file__), "runner.py"), next_task)) + except ShellCmdException as exc: + print("Failed while running the test runner: %s", exc) + errtext = exc.__str__() + send_report(next_task, out, errtext) + read_next_task_by_state(config.TASKS.INPROGRESS, next_task) + else: + print("No task") + + shellutils.unlockfile(lock_file) + + +if __name__ == "__main__": + main() diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/log/.create b/yocto-poky/bitbake/lib/toaster/contrib/tts/log/.create new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/log/.create diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/recv.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/recv.py new file mode 100755 index 000000000..07efdac44 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/recv.py @@ -0,0 +1,56 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + +# Program to receive review requests by email and log tasks to backlog.txt +# Designed to be run by the email system from a .forward file: +# +# cat .forward +# |[full/path]/recv.py + +from __future__ import print_function +import sys, config, shellutils + +from email.parser import Parser + +def recv_mail(datastring): + headers = Parser().parsestr(datastring) + return headers['subject'] + +def main(): + lock_file = shellutils.lockfile(shellutils.mk_lock_filename(), retry=True) + + if lock_file is None: + if config.DEBUG: + print("Concurrent script in progress, exiting") + sys.exit(1) + + subject = recv_mail(sys.stdin.read()) + + subject_parts = subject.split() + if "[review-request]" in subject_parts: + task_name = subject_parts[subject_parts.index("[review-request]") + 1] + with open(config.BACKLOGFILE, "a") as fout: + line = "%s|%s\n" % (task_name, config.TASKS.PENDING) + fout.write(line) + + shellutils.unlockfile(lock_file) + +if __name__ == "__main__": + main() diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/runner.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/runner.py new file mode 100755 index 000000000..bed665196 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/runner.py @@ -0,0 +1,222 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + + +# This is the main test execution controller. It is designed to be run +# manually from the command line, or to be called from a different program +# that schedules test execution. +# +# Execute runner.py -h for help. + + + +from __future__ import print_function +import sys, os +import unittest, importlib +import logging, pprint, json +import re +from shellutils import ShellCmdException, mkdirhier, run_shell_cmd + +import config + +# we also log to a file, in addition to console, because our output is important +__log_file_name__ = os.path.join(os.path.dirname(__file__), "log/tts_%d.log" % config.OWN_PID) +mkdirhier(os.path.dirname(__log_file_name__)) +__log_file__ = open(__log_file_name__, "w") +__file_handler__ = logging.StreamHandler(__log_file__) +__file_handler__.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s")) + +config.logger.addHandler(__file_handler__) + +# set up log directory +try: + if not os.path.exists(config.LOGDIR): + os.mkdir(config.LOGDIR) + else: + if not os.path.isdir(config.LOGDIR): + raise Exception("Expected log dir '%s' is not actually a directory." % config.LOGDIR) +except OSError as exc: + raise exc + +# creates the under-test-branch as a separate directory +def set_up_test_branch(settings, branch_name): + testdir = "%s/%s.%d" % (settings['workdir'], config.TEST_DIR_NAME, config.OWN_PID) + + # creates the host dir + if os.path.exists(testdir): + raise Exception("Test dir '%s'is already there, aborting" % testdir) + + # may raise OSError, is to be handled by the caller + os.makedirs(testdir) + + + # copies over the .git from the localclone + run_shell_cmd("cp -a '%s'/.git '%s'" % (settings['localclone'], testdir)) + + # add the remote if it doesn't exist + crt_remotes = run_shell_cmd("git remote -v", cwd=testdir) + remotes = [word for line in crt_remotes.split("\n") for word in line.split()] + if not config.CONTRIB_REPO in remotes: + remote_name = "tts_contrib" + run_shell_cmd("git remote add %s %s" % (remote_name, config.CONTRIB_REPO), cwd=testdir) + else: + remote_name = remotes[remotes.index(config.CONTRIB_REPO) - 1] + + # do the fetch + run_shell_cmd("git fetch %s -p" % remote_name, cwd=testdir) + + # do the checkout + run_shell_cmd("git checkout origin/master && git branch -D %s; git checkout %s/%s -b %s && git reset --hard" % (branch_name, remote_name, branch_name, branch_name), cwd=testdir) + + return testdir + + +def __search_for_tests(): + # we find all classes that can run, and run them + tests = [] + for _, _, files_list in os.walk(os.path.dirname(os.path.abspath(__file__))): + for module_file in [f[:-3] for f in files_list if f.endswith(".py") and not f.startswith("__init__")]: + config.logger.debug("Inspecting module %s", module_file) + current_module = importlib.import_module(module_file) + crtclass_names = vars(current_module) + for name in crtclass_names: + tested_value = crtclass_names[name] + if isinstance(tested_value, type(unittest.TestCase)) and issubclass(tested_value, unittest.TestCase): + tests.append((module_file, name)) + break + return tests + + +# boilerplate to self discover tests and run them +def execute_tests(dir_under_test, testname): + + if testname is not None and "." in testname: + tests = [] + tests.append(tuple(testname.split(".", 2))) + else: + tests = __search_for_tests() + + # let's move to the directory under test + crt_dir = os.getcwd() + os.chdir(dir_under_test) + + # execute each module + # pylint: disable=broad-except + # we disable the broad-except because we want to actually catch all possible exceptions + try: + # sorting the tests by the numeric order in the class name + tests = sorted(tests, key=lambda x: int(re.search(r"[0-9]+", x[1]).group(0))) + config.logger.debug("Discovered test clases: %s", pprint.pformat(tests)) + unittest.installHandler() + suite = unittest.TestSuite() + loader = unittest.TestLoader() + result = unittest.TestResult() + result.failfast = True + for module_file, test_name in tests: + suite.addTest(loader.loadTestsFromName("%s.%s" % (module_file, test_name))) + config.logger.info("Running %d test(s)", suite.countTestCases()) + suite.run(result) + + for error in result.errors: + config.logger.error("Exception on test: %s\n%s", error[0], + "\n".join(["-- %s" % x for x in error[1].split("\n")])) + + for failure in result.failures: + config.logger.error("Failed test: %s:\n%s\n", failure[0], + "\n".join(["-- %s" % x for x in failure[1].split("\n")])) + + config.logger.info("Test results: %d ran, %d errors, %d failures", result.testsRun, len(result.errors), len(result.failures)) + + except Exception as exc: + import traceback + config.logger.error("Exception while running test. Tracedump: \n%s", traceback.format_exc(exc)) + finally: + os.chdir(crt_dir) + return len(result.failures) + +# verify that we had a branch-under-test name as parameter +def validate_args(): + from optparse import OptionParser + parser = OptionParser(usage="usage: %prog [options] branch_under_test") + + parser.add_option("-t", "--test-dir", dest="testdir", default=None, help="Use specified directory to run tests, inhibits the checkout.") + parser.add_option("-s", "--single", dest="singletest", default=None, help="Run only the specified test") + + (options, args) = parser.parse_args() + if len(args) < 1: + raise Exception("Please specify the branch to run on. Use option '-h' when in doubt.") + return (options, args) + + + + +# load the configuration options +def read_settings(): + if not os.path.exists(config.SETTINGS_FILE) or not os.path.isfile(config.SETTINGS_FILE): + raise Exception("Config file '%s' cannot be openend" % config.SETTINGS_FILE) + return json.loads(open(config.SETTINGS_FILE, "r").read()) + + +# cleanup ! +def clean_up(testdir): + run_shell_cmd("rm -rf -- '%s'" % testdir) + +def dump_info(settings, options, args): + """ detailed information about current run configuration, for debugging purposes. + """ + config.logger.debug("Settings:\n%s\nOptions:\n%s\nArguments:\n%s\n", settings, options, args) + +def main(): + (options, args) = validate_args() + + settings = read_settings() + need_cleanup = False + + # dump debug info + dump_info(settings, options, args) + + testdir = None + no_failures = 1 + try: + if options.testdir is not None and os.path.exists(options.testdir): + testdir = os.path.abspath(options.testdir) + config.logger.info("No checkout, using %s", testdir) + else: + need_cleanup = True + testdir = set_up_test_branch(settings, args[0]) # we expect a branch name as first argument + + config.TESTDIR = testdir # we let tests know where to run + + # ensure that the test dir only contains no *.pyc leftovers + run_shell_cmd("find '%s' -type f -name *.pyc -exec rm {} \\;" % testdir) + + no_failures = execute_tests(testdir, options.singletest) + + except ShellCmdException as exc: + import traceback + config.logger.error("Error while setting up testing. Traceback: \n%s", traceback.format_exc(exc)) + finally: + if need_cleanup and testdir is not None: + clean_up(testdir) + + sys.exit(no_failures) + +if __name__ == "__main__": + main() diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/settings.json b/yocto-poky/bitbake/lib/toaster/contrib/tts/settings.json new file mode 100644 index 000000000..bb671eaf2 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/settings.json @@ -0,0 +1,5 @@ +{ + "repo": "git@git.yoctoproject.org:poky-contrib", + "localclone": "/home/ddalex/ssd/yocto/poky", + "workdir": "/home/ddalex/ssd/yocto" +} diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/shellutils.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/shellutils.py new file mode 100644 index 000000000..c2012edf8 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/shellutils.py @@ -0,0 +1,141 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + +# Utilities shared by tests and other common bits of code. + +import sys, os, subprocess, fcntl, errno +import config +from config import logger + + +# License warning; this code is copied from the BitBake project, file bitbake/lib/bb/utils.py +# The code is originally licensed GPL-2.0, and we redistribute it under still GPL-2.0 + +# End of copy is marked with #ENDOFCOPY marker + +def mkdirhier(directory): + """Create a directory like 'mkdir -p', but does not complain if + directory already exists like os.makedirs + """ + + try: + os.makedirs(directory) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise exc + +def lockfile(name, shared=False, retry=True): + """ + Use the file fn as a lock file, return when the lock has been acquired. + Returns a variable to pass to unlockfile(). + """ + config.logger.debug("take lockfile %s", name) + dirname = os.path.dirname(name) + mkdirhier(dirname) + + if not os.access(dirname, os.W_OK): + logger.error("Unable to acquire lock '%s', directory is not writable", + name) + sys.exit(1) + + operation = fcntl.LOCK_EX + if shared: + operation = fcntl.LOCK_SH + if not retry: + operation = operation | fcntl.LOCK_NB + + while True: + # If we leave the lockfiles lying around there is no problem + # but we should clean up after ourselves. This gives potential + # for races though. To work around this, when we acquire the lock + # we check the file we locked was still the lock file on disk. + # by comparing inode numbers. If they don't match or the lockfile + # no longer exists, we start again. + + # This implementation is unfair since the last person to request the + # lock is the most likely to win it. + + # pylint: disable=broad-except + # we disable the broad-except because we want to actually catch all possible exceptions + try: + lock_file = open(name, 'a+') + fileno = lock_file.fileno() + fcntl.flock(fileno, operation) + statinfo = os.fstat(fileno) + if os.path.exists(lock_file.name): + statinfo2 = os.stat(lock_file.name) + if statinfo.st_ino == statinfo2.st_ino: + return lock_file + lock_file.close() + except Exception as exc: + try: + lock_file.close() + except Exception as exc2: + config.logger.error("Failed to close the lockfile: %s", exc2) + config.logger.error("Failed to acquire the lockfile: %s", exc) + if not retry: + return None + +def unlockfile(lock_file): + """ + Unlock a file locked using lockfile() + """ + try: + # If we had a shared lock, we need to promote to exclusive before + # removing the lockfile. Attempt this, ignore failures. + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) + os.unlink(lock_file.name) + except (IOError, OSError): + pass + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + lock_file.close() + +#ENDOFCOPY + + +def mk_lock_filename(): + our_name = os.path.basename(__file__) + our_name = ".%s" % ".".join(reversed(our_name.split("."))) + return config.LOCKFILE + our_name + + + +class ShellCmdException(Exception): + pass + +def run_shell_cmd(command, cwd=None): + if cwd is None: + cwd = os.getcwd() + + config.logger.debug("_shellcmd: (%s) %s", cwd, command) + process = subprocess.Popen(command, cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = process.communicate() + process.wait() + if process.returncode: + if len(err) == 0: + err = "command: %s \n%s" % (command, out) + else: + err = "command: %s \n%s" % (command, err) + config.logger.warn("_shellcmd: error \n%s\n%s", out, err) + raise ShellCmdException(err) + else: + #config.logger.debug("localhostbecontroller: shellcmd success\n%s" % out) + return out + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/tests.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/tests.py new file mode 100644 index 000000000..c510ebb10 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/tests.py @@ -0,0 +1,115 @@ +#!/usr/bin/python + +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2015 Alexandru Damian for Intel Corp. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 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. + + +# Test definitions. The runner will look for and auto-discover the tests +# no matter what they file are they in, as long as they are in the same directory +# as this file. + +import unittest +from shellutils import run_shell_cmd, ShellCmdException +import config + +import pexpect +import sys, os, signal, time + +class Test00PyCompilable(unittest.TestCase): + ''' Verifies that all Python files are syntactically correct ''' + def test_compile_file(self): + try: + run_shell_cmd("find . -name *py -type f -print0 | xargs -0 -n1 -P20 python -m py_compile", config.TESTDIR) + except ShellCmdException as exc: + self.fail("Error compiling python files: %s" % (exc)) + + def test_pylint_file(self): + try: + run_shell_cmd(r"find . -iname \"*\.py\" -type f -print0 | PYTHONPATH=${PYTHONPATH}:. xargs -r -0 -n1 pylint --load-plugins pylint_django -E --reports=n 2>&1", cwd=config.TESTDIR + "/bitbake/lib/toaster") + except ShellCmdException as exc: + self.fail("Pylint fails: %s\n" % exc) + +class Test01PySystemStart(unittest.TestCase): + ''' Attempts to start Toaster, verify that it is succesfull, and stop it ''' + def setUp(self): + run_shell_cmd("bash -c 'rm -f build/*log'") + + def test_start_interactive_mode(self): + try: + run_shell_cmd("bash -c 'source %s/oe-init-build-env && source toaster start webport=%d && source toaster stop'" % (config.TESTDIR, config.TOASTER_PORT), config.TESTDIR) + except ShellCmdException as exc: + self.fail("Failed starting interactive mode: %s" % (exc)) + + def test_start_managed_mode(self): + try: + run_shell_cmd("%s/bitbake/bin/toaster webport=%d nobrowser & sleep 10 && curl http://localhost:%d/ && kill -2 %%1" % (config.TESTDIR, config.TOASTER_PORT, config.TOASTER_PORT), config.TESTDIR) + except ShellCmdException as exc: + self.fail("Failed starting managed mode: %s" % (exc)) + +class Test02HTML5Compliance(unittest.TestCase): + def setUp(self): + self.origdir = os.getcwd() + self.crtdir = os.path.dirname(config.TESTDIR) + self.cleanup_database = False + os.chdir(self.crtdir) + if not os.path.exists(os.path.join(self.crtdir, "toaster.sqlite")): + self.cleanup_database = True + run_shell_cmd("%s/bitbake/lib/toaster/manage.py syncdb --noinput" % config.TESTDIR) + run_shell_cmd("%s/bitbake/lib/toaster/manage.py migrate orm" % config.TESTDIR) + run_shell_cmd("%s/bitbake/lib/toaster/manage.py migrate bldcontrol" % config.TESTDIR) + run_shell_cmd("%s/bitbake/lib/toaster/manage.py loadconf %s/meta-yocto/conf/toasterconf.json" % (config.TESTDIR, config.TESTDIR)) + run_shell_cmd("%s/bitbake/lib/toaster/manage.py lsupdates" % config.TESTDIR) + + setup = pexpect.spawn("%s/bitbake/lib/toaster/manage.py checksettings" % config.TESTDIR) + setup.logfile = sys.stdout + setup.expect(r".*or type the full path to a different directory: ") + setup.sendline('') + setup.sendline('') + setup.expect(r".*or type the full path to a different directory: ") + setup.sendline('') + setup.expect(r"Enter your option: ") + setup.sendline('0') + + self.child = pexpect.spawn("bash", ["%s/bitbake/bin/toaster" % config.TESTDIR, "webport=%d" % config.TOASTER_PORT, "nobrowser"], cwd=self.crtdir) + self.child.logfile = sys.stdout + self.child.expect("Toaster is now running. You can stop it with Ctrl-C") + + def test_html5_compliance(self): + import urllist, urlcheck + results = {} + for url in urllist.URLS: + results[url] = urlcheck.validate_html5(config.TOASTER_BASEURL + url) + + failed = [] + for url in results: + if results[url][1] != 0: + failed.append((url, results[url])) + + + self.assertTrue(len(failed) == 0, "Not all URLs validate: \n%s " % "\n".join(["".join(str(x)) for x in failed])) + + #(config.TOASTER_BASEURL + url, status, errors, warnings)) + + def tearDown(self): + while self.child.isalive(): + self.child.kill(signal.SIGINT) + time.sleep(1) + os.chdir(self.origdir) + toaster_sqlite_path = os.path.join(self.crtdir, "toaster.sqlite") + if self.cleanup_database and os.path.exists(toaster_sqlite_path): + os.remove(toaster_sqlite_path) diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/run_toastertests.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/run_toastertests.py new file mode 100755 index 000000000..880487cb6 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/run_toastertests.py @@ -0,0 +1,87 @@ +#!/usr/bin/python + +# Copyright + +# DESCRIPTION +# This is script for running all selected toaster cases on +# selected web browsers manifested in toaster_test.cfg. + +# 1. How to start toaster in yocto: +# $ source poky/oe-init-build-env +# $ source toaster start +# $ bitbake core-image-minimal + +# 2. How to install selenium on Ubuntu: +# $ sudo apt-get install scrot python-pip +# $ sudo pip install selenium + +# 3. How to install selenium addon in firefox: +# Download the lastest firefox addon from http://release.seleniumhq.org/selenium-ide/ +# Then install it. You can also install firebug and firepath addon + +# 4. How to start writing a new case: +# All you need to do is to implement the function test_xxx() and pile it on. + +# 5. How to test with Chrome browser +# Download/install chrome on host +# Download chromedriver from https://code.google.com/p/chromedriver/downloads/list according to your host type +# put chromedriver in PATH, (e.g. /usr/bin/, bear in mind to chmod) +# For windows host, you may put chromedriver.exe in the same directory as chrome.exe + + +import unittest, time, re, sys, getopt, os, logging, platform +import ConfigParser +import subprocess + + +class toaster_run_all(): + def __init__(self): + # in case this script is called from other directory + os.chdir(os.path.abspath(sys.path[0])) + self.starttime = time.strptime(time.ctime()) + self.parser = ConfigParser.SafeConfigParser() + found = self.parser.read('toaster_test.cfg') + self.host_os = platform.system().lower() + self.run_all_cases() + self.collect_log() + + def get_test_cases(self): + # we have config groups for different os type in toaster_test.cfg + cases_to_run = eval(self.parser.get('toaster_test_' + self.host_os, 'test_cases')) + return cases_to_run + + + def run_all_cases(self): + cases_temp = self.get_test_cases() + for case in cases_temp: + single_case_cmd = "python -m unittest toaster_automation_test.toaster_cases.test_" + str(case) + print single_case_cmd + subprocess.call(single_case_cmd, shell=True) + + def collect_log(self): + """ + the log files are temporarily stored in ./log/tmp/.. + After all cases are done, they should be transfered to ./log/$TIMESTAMP/ + """ + def comple(number): + if number < 10: + return str(0) + str(number) + else: + return str(number) + now = self.starttime + now_str = comple(now.tm_year) + comple(now.tm_mon) + comple(now.tm_mday) + \ + comple(now.tm_hour) + comple(now.tm_min) + comple(now.tm_sec) + log_dir = os.path.abspath(sys.path[0]) + os.sep + 'log' + os.sep + now_str + log_tmp_dir = os.path.abspath(sys.path[0]) + os.sep + 'log' + os.sep + 'tmp' + try: + os.renames(log_tmp_dir, log_dir) + except OSError : + logging.error(" Cannot create log dir(timestamp) under log, please check your privilege") + + +if __name__ == "__main__": + toaster_run_all() + + + + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py new file mode 100755 index 000000000..2a2078f08 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py @@ -0,0 +1,1883 @@ +#!/usr/bin/python +# Copyright + +# DESCRIPTION +# This is toaster automation base class and test cases file + +# History: +# 2015.03.09 inital version +# 2015.03.23 adding toaster_test.cfg, run_toastertest.py so we can run case by case from outside + +# Briefs: +# This file is comprised of 3 parts: +# I: common utils like sorting, getting attribute.. etc +# II: base class part, which complies with unittest frame work and +# contains class selenium-based functions +# III: test cases +# to add new case: just implement new test_xxx() function in class toaster_cases + +# NOTES for cases: +# case 946: +# step 6 - 8 needs to be observed using screenshots +# case 956: +# step 2 - 3 needs to be run manually + +import unittest, time, re, sys, getopt, os, logging, string, errno, exceptions +import shutil, argparse, ConfigParser, platform +from selenium import webdriver +from selenium.common.exceptions import NoSuchElementException +from selenium import selenium +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import Select + + +########################################### +# # +# PART I: utils stuff # +# # +########################################### + +class Listattr(object): + """ + Set of list attribute. This is used to determine what the list content is. + Later on we may add more attributes here. + """ + NULL = "null" + NUMBERS = "numbers" + STRINGS = "strings" + PERCENT = "percentage" + SIZE = "size" + UNKNOWN = "unknown" + + +def get_log_root_dir(): + max_depth = 5 + parent_dir = '../' + for number in range(0, max_depth): + if os.path.isdir(sys.path[0] + os.sep + (os.pardir + os.sep)*number + 'log'): + log_root_dir = os.path.abspath(sys.path[0] + os.sep + (os.pardir + os.sep)*number + 'log') + break + + if number == (max_depth - 1): + print 'No log dir found. Please check' + raise Exception + + return log_root_dir + + +def mkdir_p(dir): + try: + os.makedirs(dir) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(dir): + pass + else: + raise + + +def get_list_attr(testlist): + """ + To determine the list content + """ + if not testlist: + return Listattr.NULL + listtest = testlist[:] + try: + listtest.remove('') + except ValueError: + pass + pattern_percent = re.compile(r"^([0-9])+(\.)?([0-9])*%$") + pattern_size = re.compile(r"^([0-9])+(\.)?([0-9])*( )*(K)*(M)*(G)*B$") + pattern_number = re.compile(r"^([0-9])+(\.)?([0-9])*$") + def get_patterned_number(pattern, tlist): + count = 0 + for item in tlist: + if re.search(pattern, item): + count += 1 + return count + if get_patterned_number(pattern_percent, listtest) == len(listtest): + return Listattr.PERCENT + elif get_patterned_number(pattern_size, listtest) == len(listtest): + return Listattr.SIZE + elif get_patterned_number(pattern_number, listtest) == len(listtest): + return Listattr.NUMBERS + else: + return Listattr.STRINGS + + +def is_list_sequenced(testlist): + """ + Function to tell if list is sequenced + Currently we may have list made up of: Strings ; numbers ; percentage ; time; size + Each has respective way to determine if it's sequenced. + """ + test_list = testlist[:] + try: + test_list.remove('') + except ValueError: + pass + + if get_list_attr(testlist) == Listattr.NULL : + return True + + elif get_list_attr(testlist) == Listattr.STRINGS : + return (sorted(test_list) == test_list) + + elif get_list_attr(testlist) == Listattr.NUMBERS : + list_number = [] + for item in test_list: + list_number.append(eval(item)) + return (sorted(list_number) == list_number) + + elif get_list_attr(testlist) == Listattr.PERCENT : + list_number = [] + for item in test_list: + list_number.append(eval(item.strip('%'))) + return (sorted(list_number) == list_number) + + elif get_list_attr(testlist) == Listattr.SIZE : + list_number = [] + # currently SIZE is splitted by space + for item in test_list: + if item.split()[1].upper() == "KB": + list_number.append(1024 * eval(item.split()[0])) + elif item.split()[1].upper() == "MB": + list_number.append(1024 * 1024 * eval(item.split()[0])) + elif item.split()[1].upper() == "GB": + list_number.append(1024 * 1024 * 1024 * eval(item.split()[0])) + else: + list_number.append(eval(item.split()[0])) + return (sorted(list_number) == list_number) + + else: + print 'Unrecognized list type, please check' + return False + + +def is_list_inverted(testlist): + """ + Function to tell if list is inverted + Currently we may have list made up of: Strings ; numbers ; percentage ; time; size + Each has respective way to determine if it's inverted. + """ + test_list = testlist[:] + try: + test_list.remove('') + except ValueError: + pass + + if get_list_attr(testlist) == Listattr.NULL : + return True + + elif get_list_attr(testlist) == Listattr.STRINGS : + return (sorted(test_list, reverse = True) == test_list) + + elif get_list_attr(testlist) == Listattr.NUMBERS : + list_number = [] + for item in test_list: + list_number.append(eval(item)) + return (sorted(list_number, reverse = True) == list_number) + + elif get_list_attr(testlist) == Listattr.PERCENT : + list_number = [] + for item in test_list: + list_number.append(eval(item.strip('%'))) + return (sorted(list_number, reverse = True) == list_number) + + elif get_list_attr(testlist) == Listattr.SIZE : + list_number = [] + # currently SIZE is splitted by space. such as 0 B; 1 KB; 2 MB + for item in test_list: + if item.split()[1].upper() == "KB": + list_number.append(1024 * eval(item.split()[0])) + elif item.split()[1].upper() == "MB": + list_number.append(1024 * 1024 * eval(item.split()[0])) + elif item.split()[1].upper() == "GB": + list_number.append(1024 * 1024 * 1024 * eval(item.split()[0])) + else: + list_number.append(eval(item.split()[0])) + return (sorted(list_number, reverse = True) == list_number) + + else: + print 'Unrecognized list type, please check' + return False + +def replace_file_content(filename, item, option): + f = open(filename) + lines = f.readlines() + f.close() + output = open(filename, 'w') + for line in lines: + if line.startswith(item): + output.write(item + " = '" + option + "'\n") + else: + output.write(line) + output.close() + +def extract_number_from_string(s): + """ + extract the numbers in a string. return type is 'list' + """ + return re.findall(r'([0-9]+)', s) + + + +########################################### +# # +# PART II: base class # +# # +########################################### + +class toaster_cases_base(unittest.TestCase): + + def setUp(self): + self.screenshot_sequence = 1 + self.verificationErrors = [] + self.accept_next_alert = True + self.host_os = platform.system().lower() + self.parser = ConfigParser.SafeConfigParser() + configs = self.parser.read('toaster_test.cfg') + self.base_url = eval(self.parser.get('toaster_test_' + self.host_os, 'toaster_url')) + + # create log dir . Currently , we put log files in log/tmp. After all + # test cases are done, move them to log/$datetime dir + self.log_tmp_dir = os.path.abspath(sys.path[0]) + os.sep + 'log' + os.sep + 'tmp' + try: + mkdir_p(self.log_tmp_dir) + except OSError : + logging.error("%(asctime)s Cannot create tmp dir under log, please check your privilege") + self.log = self.logger_create() + # driver setup + self.setup_browser() + + def logger_create(self): + """ + we use root logger for every testcase. + The reason why we don't use TOASTERXXX_logger is to avoid setting respective level for + root logger and TOASTERXXX_logger + To Be Discussed + """ + log_level_dict = {'CRITICAL':logging.CRITICAL, 'ERROR':logging.ERROR, 'WARNING':logging.WARNING, \ + 'INFO':logging.INFO, 'DEBUG':logging.DEBUG, 'NOTSET':logging.NOTSET} + log = logging.getLogger() +# log = logging.getLogger('TOASTER_' + str(self.case_no)) + self.logging_level = eval(self.parser.get('toaster_test_' + self.host_os, 'logging_level')) + key = self.logging_level.upper() + log.setLevel(log_level_dict[key]) + fh = logging.FileHandler(filename=self.log_tmp_dir + os.sep + 'case_all' + '.log', mode='a') + ch = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter('%(pathname)s - %(lineno)d - %(asctime)s \n \ + %(name)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + ch.setFormatter(formatter) + log.addHandler(fh) + log.addHandler(ch) + return log + + + def setup_browser(self, *browser_path): + self.browser = eval(self.parser.get('toaster_test_' + self.host_os, 'test_browser')) + print self.browser + if self.browser == "firefox": + driver = webdriver.Firefox() + elif self.browser == "chrome": + driver = webdriver.Chrome() + elif self.browser == "ie": + driver = webdriver.Ie() + else: + driver = None + print "unrecognized browser type, please check" + self.driver = driver + self.driver.implicitly_wait(30) + return self.driver + + + def save_screenshot(self, **log_args): + """ + This function is used to save screen either by os interface or selenium interface. + How to use: + self.save_screenshot(screenshot_type = 'native'/'selenium', log_sub_dir = 'xxx', + append_name = 'stepx') + where native means screenshot func provided by OS, + selenium means screenshot func provided by selenium webdriver + """ + types = [log_args.get('screenshot_type')] + # when no screenshot_type is specified + if types == [None]: + types = ['native', 'selenium'] + # normally append_name is used to specify which step.. + add_name = log_args.get('append_name') + if not add_name: + add_name = '-' + # normally there's no need to specify sub_dir + sub_dir = log_args.get('log_sub_dir') + if not sub_dir: + # use casexxx as sub_dir name + sub_dir = 'case' + str(self.case_no) + for item in types: + log_dir = self.log_tmp_dir + os.sep + sub_dir + mkdir_p(log_dir) + log_path = log_dir + os.sep + self.browser + '-' +\ + item + '-' + add_name + '-' + str(self.screenshot_sequence) + '.png' + if item == 'native': + os.system("scrot " + log_path) + elif item == 'selenium': + self.driver.get_screenshot_as_file(log_path) + self.screenshot_sequence += 1 + + def browser_delay(self): + """ + currently this is a workaround for some chrome test. + Sometimes we need a delay to accomplish some operation. + But for firefox, mostly we don't need this. + To be discussed + """ + if self.browser == "chrome": + time.sleep(1) + return + + +# these functions are not contained in WebDriver class.. + def find_element_by_text(self, string): + return self.driver.find_element_by_xpath("//*[text()='" + string + "']") + + + def find_elements_by_text(self, string): + return self.driver.find_elements_by_xpath("//*[text()='" + string + "']") + + + def find_element_by_text_in_table(self, table_id, text_string): + """ + This is used to search some certain 'text' in certain table + """ + try: + table_element = self.get_table_element(table_id) + element = table_element.find_element_by_xpath("//*[text()='" + text_string + "']") + except NoSuchElementException, e: + print 'no element found' + raise + return element + + + def find_element_by_link_text_in_table(self, table_id, link_text): + """ + Assume there're multiple suitable "find_element_by_link_text". + In this circumstance we need to specify "table". + """ + try: + table_element = self.get_table_element(table_id) + element = table_element.find_element_by_link_text(link_text) + except NoSuchElementException, e: + print 'no element found' + raise + return element + + + def find_elements_by_link_text_in_table(self, table_id, link_text): + """ + Search link-text in certain table. This helps to narrow down search area. + """ + try: + table_element = self.get_table_element(table_id) + element_list = table_element.find_elements_by_link_text(link_text) + except NoSuchElementException, e: + print 'no element found' + raise + return element_list + + + def find_element_by_partial_link_text_in_table(self, table_id, link_text): + """ + Search element by partial link text in certain table. + """ + try: + table_element = self.get_table_element(table_id) + element = table_element.find_element_by_partial_link_text(link_text) + return element + except NoSuchElementException, e: + print 'no element found' + raise + + + def find_elements_by_partial_link_text_in_table(self, table_id, link_text): + """ + Assume there're multiple suitable "find_partial_element_by_link_text". + """ + try: + table_element = self.get_table_element(table_id) + element_list = table_element.find_elements_by_partial_link_text(link_text) + return element_list + except NoSuchElementException, e: + print 'no element found' + raise + + + def find_element_by_xpath_in_table(self, table_id, xpath): + """ + This helps to narrow down search area. Especially useful when dealing with pop-up form. + """ + try: + table_element = self.get_table_element(table_id) + element = table_element.find_element_by_xpath(xpath) + except NoSuchElementException, e: + print 'no element found' + raise + return element + + + def find_elements_by_xpath_in_table(self, table_id, xpath): + """ + This helps to narrow down search area. Especially useful when dealing with pop-up form. + """ + try: + table_element = self.get_table_element(table_id) + element_list = table_element.find_elements_by_xpath(xpath) + except NoSuchElementException, e: + print 'no elements found' + raise + return element_list + + + def shortest_xpath(self, pname, pvalue): + return "//*[@" + pname + "='" + pvalue + "']" + + +#usually elements in the same column are with same class name. for instance: class="outcome" .TBD + def get_table_column_text(self, attr_name, attr_value): + c_xpath = self.shortest_xpath(attr_name, attr_value) + elements = self.driver.find_elements_by_xpath(c_xpath) + c_list = [] + for element in elements: + c_list.append(element.text) + return c_list + + + def get_table_column_text_by_column_number(self, table_id, column_number): + c_xpath = "//*[@id='" + table_id + "']//td[" + str(column_number) + "]" + elements = self.driver.find_elements_by_xpath(c_xpath) + c_list = [] + for element in elements: + c_list.append(element.text) + return c_list + + + def get_table_head_text(self, *table_id): +#now table_id is a tuple... + if table_id: + thead_xpath = "//*[@id='" + table_id[0] + "']//thead//th[text()]" + elements = self.driver.find_elements_by_xpath(thead_xpath) + c_list = [] + for element in elements: + if element.text: + c_list.append(element.text) + return c_list +#default table on page + else: + return self.driver.find_element_by_xpath("//*/table/thead").text + + + + def get_table_element(self, table_id, *coordinate): + if len(coordinate) == 0: +#return whole-table element + element_xpath = "//*[@id='" + table_id + "']" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException, e: + raise + return element + row = coordinate[0] + + if len(coordinate) == 1: +#return whole-row element + element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException, e: + return False + return element +#now we are looking for an element with specified X and Y + column = coordinate[1] + + element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException, e: + return False + return element + + + def get_table_data(self, table_id, row_count, column_count): + row = 1 + Lists = [] + while row <= row_count: + column = 1 + row_content=[] + while column <= column_count: + s= "//*[@id='" + table_id + "']/tbody/tr[" + str(row) +"]/td[" + str(column) + "]" + v = self.driver.find_element_by_xpath(s).text + row_content.append(v) + column = column + 1 + print("row_content=",row_content) + Lists.extend(row_content) + print Lists[row-1][0] + row = row + 1 + return Lists + + # The is_xxx_present functions only returns True/False + # All the log work is done in test procedure, so we can easily trace back + # using logging + def is_text_present (self, patterns): + for pattern in patterns: + if str(pattern) not in self.driver.page_source: + return False + return True + + + def is_element_present(self, how, what): + try: + self.driver.find_element(how, what) + except NoSuchElementException, e: + return False + return True + + + def is_alert_present(self): + try: self.driver.switch_to_alert() + except NoAlertPresentException, e: return False + return True + + + def close_alert_and_get_its_text(self): + try: + alert = self.driver.switch_to_alert() + alert_text = alert.text + if self.accept_next_alert: + alert.accept() + else: + alert.dismiss() + return alert_text + finally: self.accept_next_alert = True + + + def get_case_number(self): + """ + what case are we running now + """ + funcname = sys._getframe(1).f_code.co_name + caseno_str = funcname.strip('test_') + try: + caseno = int(caseno_str) + except ValueError: + print "get case number error! please check if func name is test_xxx" + return False + return caseno + + + def tearDown(self): + self.log.info(' END: CASE %s log \n\n' % str(self.case_no)) + self.driver.quit() + self.assertEqual([], self.verificationErrors) + + +################################################################### +# # +# PART III: test cases # +# please refer to # +# https://bugzilla.yoctoproject.org/tr_show_case.cgi?case_id=xxx # +# # +################################################################### + +# Note: to comply with the unittest framework, we call these test_xxx functions +# from run_toastercases.py to avoid calling setUp() and tearDown() multiple times + + +class toaster_cases(toaster_cases_base): + ############## + # CASE 901 # + ############## + def test_901(self): + # the reason why get_case_number is not in setUp function is that + # otherwise it returns "setUp" instead of "test_xxx" + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # open all columns + self.driver.find_element_by_id("edit-columns-button").click() + # adding explicitly wait for chromedriver..-_- + self.browser_delay() + self.driver.find_element_by_id("started_on").click() + self.browser_delay() + self.driver.find_element_by_id("time").click() + self.driver.find_element_by_id("edit-columns-button").click() + # dict: {lint text name : actual class name} + table_head_dict = {'Outcome':'outcome', 'Recipe':'target', 'Machine':'machine', 'Started on':'started_on', 'Completed on':'completed_on', \ + 'Errors':'errors_no', 'Warnings':'warnings_no', 'Time':'time'} + for key in table_head_dict: + try: + self.driver.find_element_by_link_text(key).click() + except Exception, e: + self.log.error("%s cannot be found on page" % key) + raise + column_list = self.get_table_column_text("class", table_head_dict[key]) + # after 1st click, the list should be either sequenced or inverted, but we don't have a "default order" here + # the point is, after another click, it should be another order + if is_list_inverted(column_list): + self.driver.find_element_by_link_text(key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_list)) + else: + self.failUnless(is_list_sequenced(column_list)) + self.driver.find_element_by_link_text(key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_inverted(column_list)) + self.log.info("case passed") + + + ############## + # CASE 902 # + ############## + def test_902(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # Could add more test patterns here in the future. Also, could search some items other than target column in future.. + patterns = ["minimal", "sato"] + for pattern in patterns: + ori_target_column_texts = self.get_table_column_text("class", "target") + print ori_target_column_texts + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys(pattern) + self.driver.find_element_by_id("search-button").click() + new_target_column_texts = self.get_table_column_text("class", "target") + # if nothing found, we still count it as "pass" + if new_target_column_texts: + for text in new_target_column_texts: + self.failUnless(text.find(pattern)) + self.driver.find_element_by_css_selector("i.icon-remove").click() + target_column_texts = self.get_table_column_text("class", "target") + self.failUnless(ori_target_column_texts == target_column_texts) + + + ############## + # CASE 903 # + ############## + def test_903(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # when opening a new page, "started_on" is not displayed by default + self.driver.find_element_by_id("edit-columns-button").click() + # currently all the delay are for chrome driver -_- + self.browser_delay() + self.driver.find_element_by_id("started_on").click() + self.driver.find_element_by_id("edit-columns-button").click() + # step 4 + items = ["Outcome", "Completed on", "Started on", "Failed tasks", "Errors", "Warnings"] + for item in items: + try: + temp_element = self.find_element_by_text_in_table('otable', item) + # this is how we find "filter icon" in the same level as temp_element(where "a" means clickable, "i" means icon) + self.failUnless(temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']")) + except Exception,e: + self.log.error(" %s cannot be found! %s" % (item, e)) + self.failIf(True) + raise + # step 5-6 + temp_element = self.find_element_by_link_text_in_table('otable', 'Outcome') + temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']").click() + self.browser_delay() + # the 2nd option, whatever it is + self.driver.find_element_by_xpath("(//input[@name='filter'])[2]").click() + # click "Apply" button + self.driver.find_element_by_xpath("//*[@id='filter_outcome']//*[text()='Apply']").click() + # save screen here + time.sleep(1) + self.save_screenshot(screenshot_type='selenium', append_name='step5') + temp_element = self.find_element_by_link_text_in_table('otable', 'Completed on') + temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']").click() + self.browser_delay() + self.driver.find_element_by_xpath("//*[@id='filter_completed_on']//*[text()='Apply']").click() + # save screen here to compare to previous one + # please note that for chrome driver, need a little break before saving + # screen here -_- + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step6') + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("core-image") + self.driver.find_element_by_id("search-button").click() + + + ############## + # CASE 904 # + ############## + def test_904(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_partial_link_text("core-image").click() + self.driver.find_element_by_link_text("Tasks").click() +# self.driver.find_element_by_link_text("All builds").click() +# self.driver.back() + self.table_name = 'otable' + # This is how we find the "default" rows-number! + rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text) + print rows_displayed + self.failUnless(self.get_table_element(self.table_name, rows_displayed)) + self.failIf(self.get_table_element(self.table_name, rows_displayed + 1)) + # Search text box background text is "Search tasks" + self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search tasks']")) + + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("busybox") + self.driver.find_element_by_id("search-button").click() + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step5') + self.driver.find_element_by_css_selector("i.icon-remove").click() + # Save screen here + self.save_screenshot(screenshot_type='selenium', append_name='step5_2') + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("cpu_used").click() + self.driver.find_element_by_id("disk_io").click() + self.driver.find_element_by_id("task_log").click() + self.driver.find_element_by_id("recipe_version").click() + self.driver.find_element_by_id("time_taken").click() + self.driver.find_element_by_css_selector("edit-columns-button").click() + # The operation is the same as case901 + # dict: {lint text name : actual class name} + table_head_dict = {'Order':'order', 'Recipe':'recipe_name', 'Task':'task_name', 'Executed':'executed', \ + 'Outcome':'outcome', 'Cache attempt':'cache_attempt', 'Time (secs)':'time_taken', 'CPU usage':'cpu_used', \ + 'Disk I/O (ms)':'disk_io'} + for key in table_head_dict: +# This is tricky here: we are doing so because there may be more than 1 +# same-name link_text in one page. So we only find element inside the table + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) +# after 1st click, the list should be either sequenced or inverted, but we don't have a "default order" here +# the point is, after another click, it should be another order +# the fist case is special:this means every item in column_list is the same, so +# after one click, either sequenced or inverted will be fine + if (is_list_inverted(column_list) and is_list_sequenced(column_list)) \ + or (not column_list) : + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + elif is_list_inverted(column_list): + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_list)) + else: + self.failUnless(is_list_sequenced(column_list)) + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_inverted(column_list)) +# step 8-10 + # filter dict: {link text name : filter table name in xpath} + filter_dict = {'Executed':'filter_executed', 'Outcome':'filter_outcome', 'Cache attempt':'filter_cache_attempt'} + for key in filter_dict: + temp_element = self.find_element_by_link_text_in_table(self.table_name, key) + # find the filter icon besides it. + # And here we must have break (1 sec) to get the popup stuff + temp_element.find_element_by_xpath("..//*[@class='icon-filter filtered']").click() + self.browser_delay() + avail_options = self.driver.find_elements_by_xpath("//*[@id='" + filter_dict[key] + "']//*[@name='filter'][not(@disabled)]") + for number in range(0, len(avail_options)): + avail_options[number].click() + self.browser_delay() + # click "Apply" + self.driver.find_element_by_xpath("//*[@id='" + filter_dict[key] + "']//*[@type='submit']").click() + # insert screen capture here + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step8') + # after the last option was clicked, we don't need operation below anymore + if number < len(avail_options)-1: + temp_element = self.find_element_by_link_text_in_table(self.table_name, key) + temp_element.find_element_by_xpath("..//*[@class='icon-filter filtered']").click() + avail_options = self.driver.find_elements_by_xpath("//*[@id='" + filter_dict[key] + "']//*[@name='filter'][not(@disabled)]") + self.browser_delay() +# step 11 + for item in ['order', 'task_name', 'executed', 'outcome', 'recipe_name', 'recipe_version']: + try: + self.find_element_by_xpath_in_table(self.table_name, "./tbody/tr[1]/*[@class='" + item + "']/a").click() + except NoSuchElementException, e: + # let it go... + print 'no item in the colum' + item + # insert screen shot here + self.save_screenshot(screenshot_type='selenium', append_name='step11') + self.driver.back() +# step 12-14 + # about test_dict: please refer to testcase 904 requirement step 12-14 + test_dict = { + 'Time':{ + 'class':'time_taken', + 'check_head_list':['Recipe', 'Task', 'Executed', 'Outcome', 'Time (secs)'], + 'check_column_list':['cpu_used', 'cache_attempt', 'disk_io', 'order', 'recipe_version'] + }, + 'CPU usage':{ + 'class':'cpu_used', + 'check_head_list':['Recipe', 'Task', 'Executed', 'Outcome', 'CPU usage'], + 'check_column_list':['cache_attempt', 'disk_io', 'order', 'recipe_version', 'time_taken'] + }, + 'Disk I/O':{ + 'class':'disk_io', + 'check_head_list':['Recipe', 'Task', 'Executed', 'Outcome', 'Disk I/O (ms)'], + 'check_column_list':['cpu_used', 'cache_attempt', 'order', 'recipe_version', 'time_taken'] + } + } + for key in test_dict: + self.find_element_by_partial_link_text_in_table('nav', 'core-image').click() + self.find_element_by_link_text_in_table('nav', key).click() + head_list = self.get_table_head_text('otable') + for item in test_dict[key]['check_head_list']: + self.failUnless(item in head_list) + column_list = self.get_table_column_text('class', test_dict[key]['class']) + self.failUnless(is_list_inverted(column_list)) + + self.driver.find_element_by_id("edit-columns-button").click() + for item2 in test_dict[key]['check_column_list']: + self.driver.find_element_by_id(item2).click() + self.driver.find_element_by_id("edit-columns-button").click() + # TBD: save screen here + + + ############## + # CASE 906 # + ############## + def test_906(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Packages').click() + # find "bash" in first column (Packages) + self.driver.find_element_by_xpath("//*[@id='otable']//td[1]//*[text()='bash']").click() + # save sceen here to observe... +# step 6 + self.driver.find_element_by_partial_link_text("Generated files").click() + head_list = self.get_table_head_text('otable') + for item in ['File', 'Size']: + self.failUnless(item in head_list) + c_list = self.get_table_column_text('class', 'path') + self.failUnless(is_list_sequenced(c_list)) +# step 7 + self.driver.find_element_by_partial_link_text("Runtime dependencies").click() + # save sceen here to observe... + # note that here table name is not 'otable' + head_list = self.get_table_head_text('dependencies') + for item in ['Package', 'Version', 'Size']: + self.failUnless(item in head_list) + c_list = self.get_table_column_text_by_column_number('dependencies', 1) + self.failUnless(is_list_sequenced(c_list)) + texts = ['Size', 'License', 'Recipe', 'Recipe version', 'Layer', \ + 'Layer branch', 'Layer commit', 'Layer directory'] + self.failUnless(self.is_text_present(texts)) + + + ############## + # CASE 910 # + ############## + def test_910(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + image_type="core-image-minimal" + test_package1="busybox" + test_package2="lib" + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text(image_type).click() + self.driver.find_element_by_link_text("Recipes").click() + self.save_screenshot(screenshot_type='selenium', append_name='step3') + + self.table_name = 'otable' + # This is how we find the "default" rows-number! + rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text) + print rows_displayed + self.failUnless(self.get_table_element(self.table_name, rows_displayed)) + self.failIf(self.get_table_element(self.table_name, rows_displayed + 1)) + + # Check the default table is sorted by Recipe + tasks_column_count = len(self.driver.find_elements_by_xpath("/html/body/div[2]/div/div[2]/div[2]/table/tbody/tr/td[1]")) + print tasks_column_count + default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1) + #print default_column_list + + self.failUnless(is_list_sequenced(default_column_list)) + + # Search text box background text is "Search recipes" + self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys(test_package1) + self.driver.find_element_by_id("search-button").click() + # Save screen here + self.save_screenshot(screenshot_type='selenium', append_name='step4') + self.driver.find_element_by_css_selector("i.icon-remove").click() + self.save_screenshot(screenshot_type='selenium', append_name='step4_2') + + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("depends_on").click() + self.driver.find_element_by_id("layer_version__branch").click() + self.driver.find_element_by_id("layer_version__layer__commit").click() + self.driver.find_element_by_id("depends_by").click() + self.driver.find_element_by_id("edit-columns-button").click() + + self.find_element_by_link_text_in_table(self.table_name, 'Recipe').click() + # Check the inverted table by Recipe + # Recipe doesn't have class name + #inverted_tasks_column_count = len(self.driver.find_elements_by_xpath("/html/body/div[2]/div/div[2]/div[2]/table/tbody/tr/td[1]")) + #print inverted_tasks_column_count + #inverted_column_list = self.get_table_column_text_by_column_number(self.table_name, 1) + #print inverted_column_list + + #self.driver.find_element_by_partial_link_text("zlib").click() + #self.driver.back() + #self.failUnless(is_list_inverted(inverted_column_list)) + #self.find_element_by_link_text_in_table(self.table_name, 'Recipe').click() + + table_head_dict = {'Recipe':'recipe__name', 'Recipe file':'recipe_file', 'Section':'recipe_section', \ + 'License':'recipe_license', 'Layer':'layer_version__layer__name', \ + 'Layer branch':'layer_version__branch'} + for key in table_head_dict: + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + if (is_list_inverted(column_list) and is_list_sequenced(column_list)) \ + or (not column_list) : + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + self.driver.find_element_by_partial_link_text("acl").click() + self.driver.back() + self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + # Search text box background text is "Search recipes" + self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys(test_package2) + self.driver.find_element_by_id("search-button").click() + column_search_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_search_list) or is_list_inverted(column_search_list)) + self.driver.find_element_by_css_selector("i.icon-remove").click() + elif is_list_inverted(column_list): + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_list)) + self.driver.find_element_by_partial_link_text("acl").click() + self.driver.back() + self.failUnless(is_list_sequenced(column_list)) + # Search text box background text is "Search recipes" + self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys(test_package2) + self.driver.find_element_by_id("search-button").click() + column_search_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_sequenced(column_search_list)) + self.driver.find_element_by_css_selector("i.icon-remove").click() + else: + self.failUnless(is_list_sequenced(column_list)) + self.find_element_by_link_text_in_table(self.table_name, key).click() + column_list = self.get_table_column_text("class", table_head_dict[key]) + self.failUnless(is_list_inverted(column_list)) + try: + self.driver.find_element_by_partial_link_text("acl").click() + except: + self.driver.find_element_by_partial_link_text("zlib").click() + self.driver.back() + self.failUnless(is_list_inverted(column_list)) + # Search text box background text is "Search recipes" + self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys(test_package2) + self.driver.find_element_by_id("search-button").click() + column_search_list = self.get_table_column_text("class", table_head_dict[key]) + #print column_search_list + self.failUnless(is_list_inverted(column_search_list)) + self.driver.find_element_by_css_selector("i.icon-remove").click() + + # Bug 5919 + for key in table_head_dict: + print key + self.find_element_by_link_text_in_table(self.table_name, key).click() + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id(table_head_dict[key]).click() + self.driver.find_element_by_id("edit-columns-button").click() + self.browser_delay() + # After hide the column, the default table should be sorted by Recipe + tasks_column_count = len(self.driver.find_elements_by_partial_link_text("acl")) + #print tasks_column_count + default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1) + #print default_column_list + self.failUnless(is_list_sequenced(default_column_list)) + + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("recipe_file").click() + self.driver.find_element_by_id("recipe_section").click() + self.driver.find_element_by_id("recipe_license").click() + self.driver.find_element_by_id("layer_version__layer__name").click() + self.driver.find_element_by_id("edit-columns-button").click() + + + ############## + # CASE 911 # + ############## + def test_911(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Recipes').click() +# step 3-5 + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("lib") + self.driver.find_element_by_id("search-button").click() + # save screen here for observation + self.save_screenshot(screenshot_type='selenium', append_name='step5') +# step 6 + self.driver.find_element_by_css_selector("i.icon-remove").click() + self.driver.find_element_by_id("search").clear() + # we deliberately want "no result" here + self.driver.find_element_by_id("search").send_keys("no such input") + self.driver.find_element_by_id("search-button").click() + self.find_element_by_text("Show all recipes").click() + self.driver.quit() + + + ############## + # CASE 912 # + ############## + def test_912(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver = self.setup_browser(self) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Recipes').click() + # step 3 + head_list = self.get_table_head_text('otable') + for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']: + self.failUnless(item in head_list) + self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.driver.find_element_by_id("depends_on").click() + self.driver.find_element_by_id("layer_version__branch").click() + self.driver.find_element_by_id("layer_version__layer__commit").click() + self.driver.find_element_by_id("depends_by").click() + self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + # check if columns selected above is shown + check_list = ['Dependencies', 'Layer branch', 'Layer commit', 'Layer directory', 'Reverse dependencies'] + head_list = self.get_table_head_text('otable') + time.sleep(2) + print head_list + for item in check_list: + self.failUnless(item in head_list) + # un-check 'em all + self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.driver.find_element_by_id("depends_on").click() + self.driver.find_element_by_id("layer_version__branch").click() + self.driver.find_element_by_id("layer_version__layer__commit").click() + self.driver.find_element_by_id("depends_by").click() + self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + # don't exist any more + head_list = self.get_table_head_text('otable') + for item in check_list: + self.failIf(item in head_list) + + + ############## + # CASE 913 # + ############## + def test_913(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Recipes').click() + # step 3 + head_list = self.get_table_head_text('otable') + for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']: + self.failUnless(item in head_list) + # step 4 + self.driver.find_element_by_id("edit-columns-button").click() + # save screen + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step4') + self.driver.find_element_by_id("edit-columns-button").click() + + + ############## + # CASE 914 # + ############## + def test_914(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + image_type="core-image-minimal" + test_package1="busybox" + test_package2="gdbm" + test_package3="gettext-native" + driver = self.driver + driver.maximize_window() + driver.get(self.base_url) + driver.find_element_by_link_text(image_type).click() + driver.find_element_by_link_text("Recipes").click() + driver.find_element_by_link_text(test_package1).click() + + self.table_name = 'information' + + tasks_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[1]/table/tbody/tr/td[1]")) + tasks_column_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[1]/table/tbody/tr[1]/td")) + print tasks_row_count + print tasks_column_count + + Tasks_column = self.get_table_column_text_by_column_number(self.table_name, 2) + print ("Tasks_column=", Tasks_column) + + key_tasks=["do_fetch", "do_unpack", "do_patch", "do_configure", "do_compile", "do_install", "do_package", "do_build"] + i = 0 + while i < len(key_tasks): + if key_tasks[i] not in Tasks_column: + print ("Error! Missing key task: %s" % key_tasks[i]) + else: + print ("%s is in tasks" % key_tasks[i]) + i = i + 1 + + if Tasks_column.index(key_tasks[0]) != 0: + print ("Error! %s is not in the right position" % key_tasks[0]) + else: + print ("%s is in right position" % key_tasks[0]) + + if Tasks_column[-1] != key_tasks[-1]: + print ("Error! %s is not in the right position" % key_tasks[-1]) + else: + print ("%s is in right position" % key_tasks[-1]) + + driver.find_element_by_partial_link_text("Packages (").click() + packages_name = driver.find_element_by_partial_link_text("Packages (").text + print packages_name + packages_num = string.atoi(filter(str.isdigit, repr(packages_name))) + print packages_num + + packages_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[2]/table/tbody/tr/td[1]")) + print packages_row_count + + if packages_num != packages_row_count: + print ("Error! The packages number is not correct") + else: + print ("The pakcages number is correct") + + driver.find_element_by_partial_link_text("Build dependencies (").click() + depends_name = driver.find_element_by_partial_link_text("Build dependencies (").text + print depends_name + depends_num = string.atoi(filter(str.isdigit, repr(depends_name))) + print depends_num + + if depends_num == 0: + depends_message = repr(driver.find_element_by_css_selector("div.alert.alert-info").text) + print depends_message + if depends_message.find("has no build dependencies.") < 0: + print ("Error! The message isn't expected.") + else: + print ("The message is expected") + else: + depends_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[3]/table/tbody/tr/td[1]")) + print depends_row_count + if depends_num != depends_row_count: + print ("Error! The dependent packages number is not correct") + else: + print ("The dependent packages number is correct") + + driver.find_element_by_partial_link_text("Reverse build dependencies (").click() + rdepends_name = driver.find_element_by_partial_link_text("Reverse build dependencies (").text + print rdepends_name + rdepends_num = string.atoi(filter(str.isdigit, repr(rdepends_name))) + print rdepends_num + + if rdepends_num == 0: + rdepends_message = repr(driver.find_element_by_css_selector("#brought-in-by > div.alert.alert-info").text) + print rdepends_message + if rdepends_message.find("has no reverse build dependencies.") < 0: + print ("Error! The message isn't expected.") + else: + print ("The message is expected") + else: + print ("The reverse dependent packages number is correct") + + driver.find_element_by_link_text("Recipes").click() + driver.find_element_by_link_text(test_package2).click() + driver.find_element_by_partial_link_text("Packages (").click() + driver.find_element_by_partial_link_text("Build dependencies (").click() + driver.find_element_by_partial_link_text("Reverse build dependencies (").click() + + + driver.find_element_by_link_text("Recipes").click() + driver.find_element_by_link_text(test_package3).click() + + native_tasks_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[1]/table/tbody/tr/td[1]")) + native_tasks_column_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[1]/table/tbody/tr[1]/td")) + print native_tasks_row_count + print native_tasks_column_count + + Native_Tasks_column = self.get_table_column_text_by_column_number(self.table_name, 2) + print ("Native_Tasks_column=", Native_Tasks_column) + + native_key_tasks=["do_fetch", "do_unpack", "do_patch", "do_configure", "do_compile", "do_install", "do_build"] + i = 0 + while i < len(native_key_tasks): + if native_key_tasks[i] not in Native_Tasks_column: + print ("Error! Missing key task: %s" % native_key_tasks[i]) + else: + print ("%s is in tasks" % native_key_tasks[i]) + i = i + 1 + + if Native_Tasks_column.index(native_key_tasks[0]) != 0: + print ("Error! %s is not in the right position" % native_key_tasks[0]) + else: + print ("%s is in right position" % native_key_tasks[0]) + + if Native_Tasks_column[-1] != native_key_tasks[-1]: + print ("Error! %s is not in the right position" % native_key_tasks[-1]) + else: + print ("%s is in right position" % native_key_tasks[-1]) + + driver.find_element_by_partial_link_text("Packages (").click() + native_packages_name = driver.find_element_by_partial_link_text("Packages (").text + print native_packages_name + native_packages_num = string.atoi(filter(str.isdigit, repr(native_packages_name))) + print native_packages_num + + if native_packages_num != 0: + print ("Error! Native task shouldn't have any packages.") + else: + native_package_message = repr(driver.find_element_by_css_selector("div.alert.alert-info").text) + print native_package_message + if native_package_message.find("does not build any packages.") < 0: + print ("Error! The message for native task isn't expected.") + else: + print ("The message for native task is expected.") + + driver.find_element_by_partial_link_text("Build dependencies (").click() + native_depends_name = driver.find_element_by_partial_link_text("Build dependencies (").text + print native_depends_name + native_depends_num = string.atoi(filter(str.isdigit, repr(native_depends_name))) + print native_depends_num + + native_depends_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[3]/table/tbody/tr/td[1]")) + print native_depends_row_count + + if native_depends_num != native_depends_row_count: + print ("Error! The dependent packages number is not correct") + else: + print ("The dependent packages number is correct") + + driver.find_element_by_partial_link_text("Reverse build dependencies (").click() + native_rdepends_name = driver.find_element_by_partial_link_text("Reverse build dependencies (").text + print native_rdepends_name + native_rdepends_num = string.atoi(filter(str.isdigit, repr(native_rdepends_name))) + print native_rdepends_num + + native_rdepends_row_count = len(driver.find_elements_by_xpath("/html/body/div[2]/div/div[3]/div/div[4]/table/tbody/tr/td[1]")) + print native_rdepends_row_count + + if native_rdepends_num != native_rdepends_row_count: + print ("Error! The reverse dependent packages number is not correct") + else: + print ("The reverse dependent packages number is correct") + + driver.find_element_by_link_text("Recipes").click() + + + ############## + # CASE 915 # + ############## + def test_915(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() +# step 3 + self.find_element_by_link_text_in_table('nav', 'Configuration').click() + self.driver.find_element_by_link_text("BitBake variables").click() +# step 4 + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("lib") + self.driver.find_element_by_id("search-button").click() + # save screen to see result + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step4') +# step 5 + self.driver.find_element_by_css_selector("i.icon-remove").click() + head_list = self.get_table_head_text('otable') + print head_list + print len(head_list) + self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) +# step 8 + # search other string. and click "Variable" to re-sort, check if table + # head is still the same + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("poky") + self.driver.find_element_by_id("search-button").click() + self.find_element_by_link_text_in_table('otable', 'Variable').click() + head_list = self.get_table_head_text('otable') + self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) + self.find_element_by_link_text_in_table('otable', 'Variable').click() + head_list = self.get_table_head_text('otable') + self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) + + + ############## + # CASE 916 # + ############## + def test_916(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() +# step 2-3 + self.find_element_by_link_text_in_table('nav', 'Configuration').click() + self.driver.find_element_by_link_text("BitBake variables").click() + variable_list = self.get_table_column_text('class', 'variable_name') + self.failUnless(is_list_sequenced(variable_list)) +# step 4 + self.find_element_by_link_text_in_table('otable', 'Variable').click() + variable_list = self.get_table_column_text('class', 'variable_name') + self.failUnless(is_list_inverted(variable_list)) + self.find_element_by_link_text_in_table('otable', 'Variable').click() +# step 5 + # searching won't change the sequentiality + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("lib") + self.driver.find_element_by_id("search-button").click() + variable_list = self.get_table_column_text('class', 'variable_name') + self.failUnless(is_list_sequenced(variable_list)) + + + ############## + # CASE 923 # + ############## + def test_923(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # Step 2 + # default sequence in "Completed on" column is inverted + c_list = self.get_table_column_text('class', 'completed_on') + self.failUnless(is_list_inverted(c_list)) + # step 3 + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("started_on").click() + self.driver.find_element_by_id("log").click() + self.driver.find_element_by_id("time").click() + self.driver.find_element_by_id("edit-columns-button").click() + head_list = self.get_table_head_text('otable') + for item in ['Outcome', 'Target', 'Machine', 'Started on', 'Completed on', 'Failed tasks', 'Errors', 'Warnings', 'Warnings', 'Time']: + self.failUnless(item in head_list) + + + ############## + # CASE 924 # + ############## + def test_924(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # Please refer to case 924 requirement + # default sequence in "Completed on" column is inverted + c_list = self.get_table_column_text('class', 'completed_on') + self.failUnless(is_list_inverted(c_list)) + # Step 4 + # click Errors , order in "Completed on" should be disturbed. Then hide + # error column to check if order in "Completed on" can be restored + self.find_element_by_link_text_in_table('otable', 'Errors').click() + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("errors_no").click() + self.driver.find_element_by_id("edit-columns-button").click() + # Note: without time.sleep here, there'll be unpredictable error..TBD + time.sleep(1) + c_list = self.get_table_column_text('class', 'completed_on') + self.failUnless(is_list_inverted(c_list)) + + + ############## + # CASE 940 # + ############## + def test_940(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() +# Step 2-3 + self.find_element_by_link_text_in_table('nav', 'Packages').click() + check_head_list = ['Package', 'Package version', 'Size', 'Recipe'] + head_list = self.get_table_head_text('otable') + self.failUnless(head_list == check_head_list) +# Step 4 + # pulldown menu + option_ids = ['recipe__layer_version__layer__name', 'recipe__layer_version__branch', \ + 'recipe__layer_version__layer__commit', 'license', 'recipe__version'] + self.driver.find_element_by_id("edit-columns-button").click() + for item in option_ids: + if not self.driver.find_element_by_id(item).is_selected(): + self.driver.find_element_by_id(item).click() + self.driver.find_element_by_id("edit-columns-button").click() + # save screen here to observe that 'Package' and 'Package version' is + # not selectable + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step4') + + + ############## + # CASE 941 # + ############## + def test_941(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + # Step 2-3 + self.find_element_by_link_text_in_table('nav', 'Packages').click() + # column -- Package + column_list = self.get_table_column_text_by_column_number('otable', 1) + self.failUnless(is_list_sequenced(column_list)) + self.find_element_by_link_text_in_table('otable', 'Size').click() + + + ############## + # CASE 942 # + ############## + def test_942(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.driver.find_element_by_link_text("Packages").click() + #get initial table header + head_list = self.get_table_head_text('otable') + #remove the Recipe column from table header + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("recipe__name").click() + self.driver.find_element_by_id("edit-columns-button").click() + #get modified table header + new_head = self.get_table_head_text('otable') + self.failUnless(head_list > new_head) + + ############## + # CASE 943 # + ############## + def test_943(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.driver.find_element_by_link_text("Packages").click() + #search for the "bash" package -> this should definitely be present + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("bash") + self.driver.find_element_by_id("search-button").click() + #check for the search result message "XX packages found" + self.failUnless(self.is_text_present("packages found")) + + + ############## + # CASE 944 # + ############## + def test_944(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + # step 1: test Recipes page stuff + self.driver.find_element_by_link_text("Recipes").click() + # for these 3 items, default status is not-checked + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("layer_version__branch").click() + self.driver.find_element_by_id("layer_version__layer__commit").click() + self.driver.find_element_by_id("edit-columns-button").click() + # otable is the recipes table here + otable_head_text = self.get_table_head_text('otable') + for item in ["Layer", "Layer branch", "Layer commit", "Layer directory"]: + self.failIf(item not in otable_head_text) + # click the fist recipe, whatever it is + self.get_table_element("otable", 1, 1).click() + self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit", "Recipe file"])) + + # step 2: test Packages page stuff. almost same as above + self.driver.back() + self.browser_delay() + self.driver.find_element_by_link_text("Packages").click() + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("recipe__layer_version__layer__name").click() + self.driver.find_element_by_id("recipe__layer_version__branch").click() + self.driver.find_element_by_id("recipe__layer_version__layer__commit").click() + self.driver.find_element_by_id("edit-columns-button").click() + otable_head_text = self.get_table_head_text("otable") + for item in ["Layer", "Layer branch", "Layer commit"]: + self.failIf(item not in otable_head_text) + # click the fist recipe, whatever it is + self.get_table_element("otable", 1, 1).click() + self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"])) + + # step 3: test Packages core-image-minimal(images) stuff. almost same as above. Note when future element-id changes... + self.driver.back() + self.driver.find_element_by_link_text("core-image-minimal").click() + self.driver.find_element_by_id("edit-columns-button").click() + self.driver.find_element_by_id("layer_name").click() + self.driver.find_element_by_id("layer_branch").click() + self.driver.find_element_by_id("layer_commit").click() + self.driver.find_element_by_id("edit-columns-button").click() + otable_head_text = self.get_table_head_text("otable") + for item in ["Layer", "Layer branch", "Layer commit"]: + self.failIf(item not in otable_head_text) + # click the fist recipe, whatever it is + self.get_table_element("otable", 1, 1).click() + self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"])) + + # step 4: check Configuration page + self.driver.back() + self.driver.find_element_by_link_text("Configuration").click() + otable_head_text = self.get_table_head_text() + for item in ["Layer", "Layer branch", "Layer commit"]: + self.failIf(item not in otable_head_text) + + + ############## + # CASE 945 # + ############## + def test_945(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + for items in ["Packages", "Recipes", "Tasks"]: + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.driver.find_element_by_link_text(items).click() + + # this may be page specific. If future page content changes, try to replace it with new xpath + xpath_showrows = "/html/body/div[2]/div/div[2]/div[2]/div[2]/div/div/div[2]/select" + xpath_table = "/html/body/div[2]/div/div[2]/div[2]/table/tbody" + self.driver.find_element_by_xpath(xpath_showrows).click() + rows_displayed = int(self.driver.find_element_by_xpath(xpath_showrows + "/option[2]").text) + + # not sure if this is a Selenium Select bug: If page is not refreshed here, "select(by visible text)" operation will go back to 100-row page + # Sure we can use driver.get(url) to refresh page, but since page will vary, we use click link text here + self.driver.find_element_by_link_text(items).click() + Select(self.driver.find_element_by_css_selector("select.pagesize")).select_by_visible_text(str(rows_displayed)) + self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) + self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) + + # click 1st package, then go back to check if it's still those rows shown. + self.driver.find_element_by_xpath(xpath_table + "/tr[1]/td[1]").click() + self.driver.find_element_by_link_text(items).click() + self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) + self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) + + + ############## + # CASE 946 # + ############## + def test_946(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.driver.find_element_by_link_text("Configuration").click() + # step 3-4 + check_list = ["Summary", "BitBake variables"] + for item in check_list: + if not self.is_element_present(how=By.LINK_TEXT, what=item): + self.log.error("%s not found" %item) + if not self.is_text_present(['Layers', 'Layer', 'Layer branch', 'Layer commit']): + self.log.error("text not found") + # step 5 + self.driver.find_element_by_link_text("BitBake variables").click() + if not self.is_text_present(['Variable', 'Value', 'Set in file', 'Description']): + self.log.error("text not found") + # This may be unstable because it's page-specific + # step 6: this is how we find filter beside "Set in file" + temp_element = self.find_element_by_text_in_table('otable', "Set in file") + temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']").click() + self.browser_delay() + self.driver.find_element_by_xpath("(//input[@name='filter'])[3]").click() + btns = self.driver.find_elements_by_css_selector("button.btn.btn-primary") + for btn in btns: + try: + btn.click() + break + except: + pass + # save screen here + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step6') + self.driver.find_element_by_id("edit-columns-button").click() + # save screen here + # step 7 + # we should manually check the step 6-8 result using screenshot + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step7') + self.driver.find_element_by_id("edit-columns-button").click() + # step 9 + # click the 1st item, no matter what it is + self.driver.find_element_by_xpath("//*[@id='otable']/tbody/tr[1]/td[1]/a").click() + # give it 1 sec so the pop-up becomes the "active_element" + time.sleep(1) + element = self.driver.switch_to.active_element + check_list = ['Order', 'Configuration file', 'Operation', 'Line number'] + for item in check_list: + if item not in element.text: + self.log.error("%s not found" %item) + # any better way to close this pop-up? ... TBD + element.find_element_by_class_name("close").click() + # step 10 : need to manually check "Yocto Manual" in saved screen + self.driver.find_element_by_css_selector("i.icon-share.get-info").click() + # save screen here + time.sleep(5) + self.save_screenshot(screenshot_type='native', append_name='step10') + + + ############## + # CASE 947 # + ############## + def test_947(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Configuration').click() + # step 2 + self.driver.find_element_by_link_text("BitBake variables").click() + # step 3 + def xpath_option(column_name): + # return xpath of options under "Edit columns" button + return self.shortest_xpath('id', 'navTab') + self.shortest_xpath('id', 'editcol') \ + + self.shortest_xpath('id', column_name) + self.driver.find_element_by_id('edit-columns-button').click() + # by default, option "Description" and "Set in file" were checked + self.driver.find_element_by_xpath(xpath_option('description')).click() + self.driver.find_element_by_xpath(xpath_option('file')).click() + self.driver.find_element_by_id('edit-columns-button').click() + check_list = ['Description', 'Set in file'] + head_list = self.get_table_head_text('otable') + for item in check_list: + self.failIf(item in head_list) + # check these 2 options and verify again + self.driver.find_element_by_id('edit-columns-button').click() + self.driver.find_element_by_xpath(xpath_option('description')).click() + self.driver.find_element_by_xpath(xpath_option('file')).click() + self.driver.find_element_by_id('edit-columns-button').click() + head_list = self.get_table_head_text('otable') + for item in check_list: + self.failUnless(item in head_list) + + + ############## + # CASE 948 # + ############## + def test_948(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'Configuration').click() + self.driver.find_element_by_link_text("BitBake variables").click() + #get number of variables visible by default + number_before_search = self.driver.find_element_by_class_name('page-header').text + # search for a while... + self.driver.find_element_by_id("search").clear() + self.driver.find_element_by_id("search").send_keys("BB") + self.driver.find_element_by_id("search-button").click() + #get number of variables visible after search + number_after_search = self.driver.find_element_by_class_name('page-header').text + self.failUnless(number_before_search > number_after_search) + + + ############## + # CASE 949 # + ############## + def test_949(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.driver.find_element_by_link_text("core-image-minimal").click() + self.find_element_by_link_text_in_table('nav', 'core-image-minimal').click() + # step 3 + try: + self.driver.find_element_by_partial_link_text("Packages included") + self.driver.find_element_by_partial_link_text("Directory structure") + except Exception,e: + self.log.error(e) + self.failIf(True) + # step 4 + head_list = self.get_table_head_text('otable') + for item in ['Package', 'Package version', 'Size', 'Dependencies', 'Reverse dependencies', 'Recipe']: + self.failUnless(item in head_list) + # step 5-6 + self.driver.find_element_by_id("edit-columns-button").click() + selectable_class = 'checkbox' + # minimum-table : means unselectable items + unselectable_class = 'checkbox muted' + selectable_check_list = ['Dependencies', 'Layer', 'Layer branch', 'Layer commit', \ + 'License', 'Recipe', 'Recipe version', 'Reverse dependencies', \ + 'Size', 'Size over total (%)'] + unselectable_check_list = ['Package', 'Package version'] + selectable_list = list() + unselectable_list = list() + selectable_elements = self.driver.find_elements_by_xpath("//*[@id='editcol']//*[@class='" + selectable_class + "']") + unselectable_elements = self.driver.find_elements_by_xpath("//*[@id='editcol']//*[@class='" + unselectable_class + "']") + for element in selectable_elements: + selectable_list.append(element.text) + for element in unselectable_elements: + unselectable_list.append(element.text) + # check them + for item in selectable_check_list: + if item not in selectable_list: + self.log.error(" %s not found in dropdown menu \n" % item) + self.failIf(True) + for item in unselectable_check_list: + if item not in unselectable_list: + self.log.error(" %s not found in dropdown menu \n" % item) + self.failIf(True) + self.driver.find_element_by_id("edit-columns-button").click() + # step 7 + self.driver.find_element_by_partial_link_text("Directory structure").click() + head_list = self.get_table_head_text('dirtable') + for item in ['Directory / File', 'Symbolic link to', 'Source package', 'Size', 'Permissions', 'Owner', 'Group']: + if item not in head_list: + self.log.error(" %s not found in Directory structure table head \n" % item) + self.failIf(True) + + + ############## + # CASE 950 # + ############## + def test_950(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # step3&4: so far we're not sure if there's "successful build" or "failed + # build".If either of them doesn't exist, we can still go on other steps + check_list = ['Configuration', 'Tasks', 'Recipes', 'Packages', 'Time', 'CPU usage', 'Disk I/O'] + has_successful_build = 1 + has_failed_build = 1 + try: + pass_icon = self.driver.find_element_by_xpath("//*[@class='icon-ok-sign success']") + except Exception: + self.log.info("no successful build exists") + has_successful_build = 0 + pass + if has_successful_build: + pass_icon.click() + # save screen here to check if it matches requirement. + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step3_1') + for item in check_list: + try: + self.find_element_by_link_text_in_table('nav', item) + except Exception: + self.log.error("link %s cannot be found in the page" % item) + self.failIf(True) + # step 6 + check_list_2 = ['Packages included', 'Total package size', \ + 'License manifest', 'Image files'] + self.failUnless(self.is_text_present(check_list_2)) + self.driver.back() + try: + fail_icon = self.driver.find_element_by_xpath("//*[@class='icon-minus-sign error']") + except Exception: + has_failed_build = 0 + self.log.info("no failed build exists") + pass + if has_failed_build: + fail_icon.click() + # save screen here to check if it matches requirement. + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step3_2') + for item in check_list: + try: + self.find_element_by_link_text_in_table('nav', item) + except Exception: + self.log.error("link %s cannot be found in the page" % item) + self.failIf(True) + # step 7 involved + check_list_3 = ['Machine', 'Distro', 'Layers', 'Total number of tasks', 'Tasks executed', \ + 'Tasks not executed', 'Reuse', 'Recipes built', 'Packages built'] + self.failUnless(self.is_text_present(check_list_3)) + self.driver.back() + + + ############## + # CASE 951 # + ############## + def test_951(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # currently test case itself isn't responsible for creating "1 successful and + # 1 failed build" + has_successful_build = 1 + has_failed_build = 1 + try: + fail_icon = self.driver.find_element_by_xpath("//*[@class='icon-minus-sign error']") + except Exception: + has_failed_build = 0 + self.log.info("no failed build exists") + pass + # if there's failed build, we can proceed + if has_failed_build: + self.driver.find_element_by_partial_link_text("error").click() + self.driver.back() + # not sure if there "must be" some warnings, so here save a screen + self.browser_delay() + self.save_screenshot(screenshot_type='selenium', append_name='step4') + + + ############## + # CASE 955 # + ############## + def test_955(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + self.log.info(" You should manually create all images before test starts!") + # So far the case itself is not responsable for creating all sorts of images. + # So assuming they are already there + # step 2 + self.driver.find_element_by_link_text("core-image-minimal").click() + # save screen here to see the page component + + + ############## + # CASE 956 # + ############## + def test_956(self): + self.case_no = self.get_case_number() + self.log.info(' CASE %s log: ' % str(self.case_no)) + self.driver.maximize_window() + self.driver.get(self.base_url) + # step 2-3 need to run manually + self.log.info("step 2-3: checking the help message when you hover on help icon of target,\ + tasks, recipes, packages need to run manually") + self.driver.find_element_by_partial_link_text("Toaster manual").click() + if not self.is_text_present("Toaster Manual"): + self.log.error("please check [Toaster manual] link on page") + self.failIf(True) + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg new file mode 100644 index 000000000..6405f9a8e --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg @@ -0,0 +1,21 @@ +# Configuration file for toaster_test +# Sorted by different host type + +# test browser could be: firefox; chrome; ie(still under development) +# logging_level could be: CRITICAL; ERROR; WARNING; INFO; DEBUG; NOTSET + + +[toaster_test_linux] +toaster_url = 'http://127.0.0.1:8000' +test_browser = 'firefox' +test_cases = [946] +logging_level = 'INFO' + + +[toaster_test_windows] +toaster_url = 'http://127.0.0.1:8000' +test_browser = ['ie', 'firefox', 'chrome'] +test_cases = [901, 902, 903] +logging_level = 'DEBUG' + + diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/urlcheck.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/urlcheck.py new file mode 100644 index 000000000..0820f82e2 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/urlcheck.py @@ -0,0 +1,53 @@ +from __future__ import print_function +import sys + +import httplib2 +import config +import urllist + + +config.logger.info("Testing %s with %s", config.TOASTER_BASEURL, config.W3C_VALIDATOR) + +def validate_html5(url): + http_client = httplib2.Http(None) + status = "Failed" + errors = -1 + warnings = -1 + + urlrequest = config.W3C_VALIDATOR+url + + # pylint: disable=broad-except + # we disable the broad-except because we want to actually catch all possible exceptions + try: + resp, _ = http_client.request(urlrequest, "HEAD") + if resp['x-w3c-validator-status'] != "Abort": + status = resp['x-w3c-validator-status'] + errors = int(resp['x-w3c-validator-errors']) + warnings = int(resp['x-w3c-validator-warnings']) + + if status == 'Invalid': + config.logger.warn("Failed %s is %s\terrors %s warnings %s (check at %s)", url, status, errors, warnings, urlrequest) + else: + config.logger.debug("OK! %s", url) + + except Exception as exc: + config.logger.warn("Failed validation call: %s", exc) + return (status, errors, warnings) + + +def print_validation(url): + status, errors, warnings = validate_html5(url) + config.logger.error("url %s is %s\terrors %s warnings %s (check at %s)", url, status, errors, warnings, config.W3C_VALIDATOR+url) + + +def main(): + print("Testing %s with %s" % (config.TOASTER_BASEURL, config.W3C_VALIDATOR)) + + if len(sys.argv) > 1: + print_validation(sys.argv[1]) + else: + for url in urllist.URLS: + print_validation(config.TOASTER_BASEURL+url) + +if __name__ == "__main__": + main() diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/urllist.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/urllist.py new file mode 100644 index 000000000..6db9ffc7b --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/urllist.py @@ -0,0 +1,39 @@ +URLS = [ + 'toastergui/landing/', + 'toastergui/builds/', + 'toastergui/build/1', + 'toastergui/build/1/tasks/', + 'toastergui/build/1/tasks/1/', + 'toastergui/build/1/task/1', + 'toastergui/build/1/recipes/', + 'toastergui/build/1/recipe/1/active_tab/1', + 'toastergui/build/1/recipe/1', + 'toastergui/build/1/recipe_packages/1', + 'toastergui/build/1/packages/', + 'toastergui/build/1/package/1', + 'toastergui/build/1/package_built_dependencies/1', + 'toastergui/build/1/package_included_detail/1/1', + 'toastergui/build/1/package_included_dependencies/1/1', + 'toastergui/build/1/package_included_reverse_dependencies/1/1', + 'toastergui/build/1/target/1', + 'toastergui/build/1/target/1/targetpkg', + 'toastergui/build/1/target/1/dirinfo', + 'toastergui/build/1/target/1/dirinfo_filepath/_/bin/bash', + 'toastergui/build/1/configuration', + 'toastergui/build/1/configvars', + 'toastergui/build/1/buildtime', + 'toastergui/build/1/cpuusage', + 'toastergui/build/1/diskio', + 'toastergui/build/1/target/1/packagefile/1', + 'toastergui/newproject/', + 'toastergui/projects/', + 'toastergui/project/1', + 'toastergui/project/1/configuration', + 'toastergui/project/1/builds/', + 'toastergui/project/1/layers/', + 'toastergui/project/1/layer/1', + 'toastergui/project/1/importlayer', + 'toastergui/project/1/targets/', + 'toastergui/project/1/machines/', + 'toastergui/', +] |