diff options
Diffstat (limited to 'lib/toaster/tests')
37 files changed, 3055 insertions, 300 deletions
diff --git a/lib/toaster/tests/browser/selenium_helpers.py b/lib/toaster/tests/browser/selenium_helpers.py index 6d9bb8092..02d4f4b5c 100644 --- a/lib/toaster/tests/browser/selenium_helpers.py +++ b/lib/toaster/tests/browser/selenium_helpers.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # diff --git a/lib/toaster/tests/browser/selenium_helpers_base.py b/lib/toaster/tests/browser/selenium_helpers_base.py index 8417aa3b2..393be7549 100644 --- a/lib/toaster/tests/browser/selenium_helpers_base.py +++ b/lib/toaster/tests/browser/selenium_helpers_base.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -19,12 +19,15 @@ import os import time import unittest -from django.contrib.staticfiles.testing import StaticLiveServerTestCase +import pytest from selenium import webdriver +from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.common.exceptions import NoSuchElementException, \ - StaleElementReferenceException, TimeoutException + StaleElementReferenceException, TimeoutException, \ + SessionNotCreatedException def create_selenium_driver(cls,browser='chrome'): # set default browser string based on env (if available) @@ -33,9 +36,32 @@ def create_selenium_driver(cls,browser='chrome'): browser = env_browser if browser == 'chrome': - return webdriver.Chrome( - service_args=["--verbose", "--log-path=selenium.log"] - ) + options = webdriver.ChromeOptions() + options.add_argument('--headless') + options.add_argument('--disable-infobars') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--no-sandbox') + options.add_argument('--remote-debugging-port=9222') + try: + return webdriver.Chrome(options=options) + except SessionNotCreatedException as e: + exit_message = "Halting tests prematurely to avoid cascading errors." + # check if chrome / chromedriver exists + chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() + if not chrome_path: + pytest.exit(f"Failed to install/find chrome.\n{exit_message}") + chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() + if not chromedriver_path: + pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") + # check if depends on each are fulfilled + depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() + if depends_chrome: + pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") + depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() + if depends_chromedriver: + pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") + # print original error otherwise + pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") elif browser == 'firefox': return webdriver.Firefox() elif browser == 'marionette': @@ -67,7 +93,9 @@ class Wait(WebDriverWait): _TIMEOUT = 10 _POLL_FREQUENCY = 0.5 - def __init__(self, driver): + def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): + self._TIMEOUT = timeout + self._POLL_FREQUENCY = poll super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) def until(self, method, message=''): @@ -139,6 +167,8 @@ class SeleniumTestCaseBase(unittest.TestCase): """ Clean up webdriver driver """ cls.driver.quit() + # Allow driver resources to be properly freed before proceeding with further tests + time.sleep(5) super(SeleniumTestCaseBase, cls).tearDownClass() def get(self, url): @@ -152,13 +182,20 @@ class SeleniumTestCaseBase(unittest.TestCase): abs_url = '%s%s' % (self.live_server_url, url) self.driver.get(abs_url) + try: # Ensure page is loaded before proceeding + self.wait_until_visible("#global-nav", poll=3) + except NoSuchElementException: + self.driver.implicitly_wait(3) + except TimeoutException: + self.driver.implicitly_wait(3) + def find(self, selector): """ Find single element by CSS selector """ - return self.driver.find_element_by_css_selector(selector) + return self.driver.find_element(By.CSS_SELECTOR, selector) def find_all(self, selector): """ Find all elements matching CSS selector """ - return self.driver.find_elements_by_css_selector(selector) + return self.driver.find_elements(By.CSS_SELECTOR, selector) def element_exists(self, selector): """ @@ -171,18 +208,34 @@ class SeleniumTestCaseBase(unittest.TestCase): """ Return the element which currently has focus on the page """ return self.driver.switch_to.active_element - def wait_until_present(self, selector): + def wait_until_present(self, selector, poll=0.5): """ Wait until element matching CSS selector is on the page """ is_present = lambda driver: self.find(selector) msg = 'An element matching "%s" should be on the page' % selector - element = Wait(self.driver).until(is_present, msg) + element = Wait(self.driver, poll=poll).until(is_present, msg) + if poll > 2: + time.sleep(poll) # element need more delay to be present return element - def wait_until_visible(self, selector): + def wait_until_visible(self, selector, poll=1): """ Wait until element matching CSS selector is visible on the page """ is_visible = lambda driver: self.find(selector).is_displayed() msg = 'An element matching "%s" should be visible' % selector - Wait(self.driver).until(is_visible, msg) + Wait(self.driver, poll=poll).until(is_visible, msg) + time.sleep(poll) # wait for visibility to settle + return self.find(selector) + + def wait_until_clickable(self, selector, poll=1): + """ Wait until element matching CSS selector is visible on the page """ + WebDriverWait( + self.driver, + Wait._TIMEOUT, + poll_frequency=poll + ).until( + EC.element_to_be_clickable((By.ID, selector.removeprefix('#') + ) + ) + ) return self.find(selector) def wait_until_focused(self, selector): diff --git a/lib/toaster/tests/browser/test_all_builds_page.py b/lib/toaster/tests/browser/test_all_builds_page.py index f4021614b..b9356a034 100644 --- a/lib/toaster/tests/browser/test_all_builds_page.py +++ b/lib/toaster/tests/browser/test_all_builds_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,13 +7,18 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re -from django.core.urlresolvers import reverse +from django.urls import reverse +from selenium.webdriver.support.select import Select from django.utils import timezone +from bldcontrol.models import BuildRequest from tests.browser.selenium_helpers import SeleniumTestCase -from orm.models import BitbakeVersion, Release, Project, Build, Target +from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task + +from selenium.webdriver.common.by import By class TestAllBuildsPage(SeleniumTestCase): @@ -23,7 +28,8 @@ class TestAllBuildsPage(SeleniumTestCase): CLI_BUILDS_PROJECT_NAME = 'command line builds' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -69,7 +75,7 @@ class TestAllBuildsPage(SeleniumTestCase): '[data-role="data-recent-build-buildtime-field"]' % build.id # because this loads via Ajax, wait for it to be visible - self.wait_until_present(selector) + self.wait_until_visible(selector) build_time_spans = self.find_all(selector) @@ -79,7 +85,7 @@ class TestAllBuildsPage(SeleniumTestCase): def _get_row_for_build(self, build): """ Get the table row for the build from the all builds table """ - self.wait_until_present('#allbuildstable') + self.wait_until_visible('#allbuildstable') rows = self.find_all('#allbuildstable tr') @@ -91,7 +97,7 @@ class TestAllBuildsPage(SeleniumTestCase): found_row = None for row in rows: - outcome_links = row.find_elements_by_css_selector(selector) + outcome_links = row.find_elements(By.CSS_SELECTOR, selector) if len(outcome_links) == 1: found_row = row break @@ -100,6 +106,66 @@ class TestAllBuildsPage(SeleniumTestCase): return found_row + def _get_create_builds(self, **kwargs): + """ Create a build and return the build object """ + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + if kwargs: + # Create kwargs.get('success') builds with success status with target + # and kwargs.get('failure') builds with failure status with target + for i in range(kwargs.get('success', 0)): + now = timezone.now() + self.project1_build_success['started_on'] = now + self.project1_build_success[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, + target=f'{i}_success_recipe', + task=f'{i}_success_task') + + self._set_buildRequest_and_task_on_build(build) + for i in range(kwargs.get('failure', 0)): + now = timezone.now() + self.project1_build_failure['started_on'] = now + self.project1_build_failure[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_failure) + Target.objects.create(build=build, + target=f'{i}_fail_recipe', + task=f'{i}_fail_task') + self._set_buildRequest_and_task_on_build(build) + return build1, build2 + + def _create_recipe(self): + """ Add a recipe to the database and return it """ + layer = Layer.objects.create() + layer_version = Layer_Version.objects.create(layer=layer) + return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) + + def _set_buildRequest_and_task_on_build(self, build): + """ Set buildRequest and task on build """ + build.recipes_parsed = 1 + build.save() + buildRequest = BuildRequest.objects.create( + build=build, + project=self.project1, + state=BuildRequest.REQ_COMPLETED) + build.build_request = buildRequest + recipe = self._create_recipe() + task = Task.objects.create(build=build, + recipe=recipe, + task_name='task', + outcome=Task.OUTCOME_SUCCESS) + task.save() + build.save() + def test_show_tasks_with_suffix(self): """ Task should be shown as suffix on build name """ build = Build.objects.create(**self.project1_build_success) @@ -109,7 +175,7 @@ class TestAllBuildsPage(SeleniumTestCase): url = reverse('all-builds') self.get(url) - self.wait_until_present('td[class="target"]') + self.wait_until_visible('td[class="target"]') cell = self.find('td[class="target"]') content = cell.get_attribute('innerHTML') @@ -126,23 +192,25 @@ class TestAllBuildsPage(SeleniumTestCase): but should be shown for other builds """ build1 = Build.objects.create(**self.project1_build_success) - default_build = Build.objects.create(**self.default_project_build_success) + default_build = Build.objects.create( + **self.default_project_build_success) url = reverse('all-builds') self.get(url) - # shouldn't see a rebuild button for command-line builds - selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id - run_again_button = self.find_all(selector) - self.assertEqual(len(run_again_button), 0, - 'should not see a rebuild button for cli builds') - # should see a rebuild button for non-command-line builds + self.wait_until_visible('#allbuildstable tbody tr') selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id run_again_button = self.find_all(selector) self.assertEqual(len(run_again_button), 1, 'should see a rebuild button for non-cli builds') + # shouldn't see a rebuild button for command-line builds + selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 0, + 'should not see a rebuild button for cli builds') + def test_tooltips_on_project_name(self): """ Test tooltips shown next to project name in the main table @@ -156,6 +224,7 @@ class TestAllBuildsPage(SeleniumTestCase): url = reverse('all-builds') self.get(url) + self.wait_until_visible('#allbuildstable', poll=3) # get the project name cells from the table cells = self.find_all('#allbuildstable td[class="project"]') @@ -164,7 +233,7 @@ class TestAllBuildsPage(SeleniumTestCase): for cell in cells: content = cell.get_attribute('innerHTML') - help_icons = cell.find_elements_by_css_selector(selector) + help_icons = cell.find_elements(By.CSS_SELECTOR, selector) if re.search(self.PROJECT_NAME, content): # no help icon next to non-cli project name @@ -184,38 +253,224 @@ class TestAllBuildsPage(SeleniumTestCase): recent builds area; failed builds should not have links on the time column, or in the recent builds area """ - build1 = Build.objects.create(**self.project1_build_success) - build2 = Build.objects.create(**self.project1_build_failure) - - # add some targets to these builds so they have recipe links - # (and so we can find the row in the ToasterTable corresponding to - # a particular build) - Target.objects.create(build=build1, target='foo') - Target.objects.create(build=build2, target='bar') + build1, build2 = self._get_create_builds() url = reverse('all-builds') self.get(url) + self.wait_until_visible('#allbuildstable', poll=3) # test recent builds area for successful build element = self._get_build_time_element(build1) - links = element.find_elements_by_css_selector('a') + links = element.find_elements(By.CSS_SELECTOR, 'a') msg = 'should be a link on the build time for a successful recent build' - self.assertEquals(len(links), 1, msg) + self.assertEqual(len(links), 1, msg) # test recent builds area for failed build element = self._get_build_time_element(build2) - links = element.find_elements_by_css_selector('a') + links = element.find_elements(By.CSS_SELECTOR, 'a') msg = 'should not be a link on the build time for a failed recent build' - self.assertEquals(len(links), 0, msg) + self.assertEqual(len(links), 0, msg) # test the time column for successful build build1_row = self._get_row_for_build(build1) - links = build1_row.find_elements_by_css_selector('td.time a') + links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') msg = 'should be a link on the build time for a successful build' - self.assertEquals(len(links), 1, msg) + self.assertEqual(len(links), 1, msg) # test the time column for failed build build2_row = self._get_row_for_build(build2) - links = build2_row.find_elements_by_css_selector('td.time a') + links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') msg = 'should not be a link on the build time for a failed build' - self.assertEquals(len(links), 0, msg) + self.assertEqual(len(links), 0, msg) + + def test_builds_table_search_box(self): + """ Test the search box in the builds table on the all builds page """ + self._get_create_builds() + + url = reverse('all-builds') + self.get(url) + + # Check search box is present and works + self.wait_until_visible('#allbuildstable tbody tr') + search_box = self.find('#search-input-allbuildstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a build by recipe name + search_box.send_keys('foo') + search_btn = self.find('#search-submit-allbuildstable') + search_btn.click() + self.wait_until_visible('#allbuildstable tbody tr') + rows = self.find_all('#allbuildstable tbody tr') + self.assertTrue(len(rows) >= 1) + + def test_filtering_on_failure_tasks_column(self): + """ Test the filtering on failure tasks column in the builds table on the all builds page """ + def _check_if_filter_failed_tasks_column_is_visible(): + # check if failed tasks filter column is visible, if not click on it + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') + if not filter_fails_task_checkbox.is_selected(): + filter_fails_task_checkbox.click() + edit_column.click() + + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_visible('#allbuildstable tbody tr') + _check_if_filter_failed_tasks_column_is_visible() + failed_tasks_filter = self.find('#failed_tasks_filter') + failed_tasks_filter.click() + # Check popup is visible + self.wait_until_visible('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#failed_tasks_filter\\:without_failed_tasks') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class + self.assertTrue(self.find('#failed_tasks_filter').get_attribute( + 'class').find('btn-primary') != -1) + + def test_filtering_on_completedOn_column(self): + """ Test the filtering on completed_on column in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + url = reverse('all-builds') + self.get(url) + + # Check filtering on failure tasks column + self.wait_until_visible('#allbuildstable tbody tr') + completed_on_filter = self.find('#completed_on_filter') + completed_on_filter.click() + # Check popup is visible + self.wait_until_visible('#filter-modal-allbuildstable') + self.assertTrue( + self.find('#filter-modal-allbuildstable').is_displayed()) + # Check that we can filter by failure tasks + build_without_failure_tasks = self.find( + '#completed_on_filter\\:date_range') + build_without_failure_tasks.click() + # click on apply button + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + # Check if filter is applied, by checking if completed_on_filter has btn-primary class + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + + # Filter by date range + self.find('#completed_on_filter').click() + self.wait_until_visible('#filter-modal-allbuildstable') + date_ranges = self.driver.find_elements( + By.XPATH, '//input[@class="form-control hasDatepicker"]') + today = timezone.now() + yestersday = today - timezone.timedelta(days=1) + date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) + date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) + self.find('#filter-modal-allbuildstable .btn-primary').click() + self.wait_until_visible('#allbuildstable tbody tr') + self.assertTrue(self.find('#completed_on_filter').get_attribute( + 'class').find('btn-primary') != -1) + # Check if filter is applied, number of builds displayed should be 6 + self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) + + def test_builds_table_editColumn(self): + """ Test the edit column feature in the builds table on the all builds page """ + self._get_create_builds(success=10, failure=10) + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#allbuildstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#allbuildstable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors_no') + test_edit_column('checkbox-failed_tasks') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-project') + test_edit_column('checkbox-started_on') + test_edit_column('checkbox-time') + test_edit_column('checkbox-warnings_no') + + def test_builds_table_show_rows(self): + """ Test the show rows feature in the builds table on the all builds page """ + self._get_create_builds(success=100, failure=100) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#allbuildstable tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all('#allbuildstable tbody tr')) > 0 + ) + + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#allbuildstable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-allbuildstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/lib/toaster/tests/browser/test_all_projects_page.py b/lib/toaster/tests/browser/test_all_projects_page.py index f86d19d28..9ed1901cc 100644 --- a/lib/toaster/tests/browser/test_all_projects_page.py +++ b/lib/toaster/tests/browser/test_all_projects_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,15 +7,20 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone +from selenium.webdriver.support.select import Select from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, Build from orm.models import ProjectVariable +from selenium.webdriver.common.by import By + + class TestAllProjectsPage(SeleniumTestCase): """ Browser tests for projects page /projects/ """ @@ -25,7 +30,8 @@ class TestAllProjectsPage(SeleniumTestCase): def setUp(self): """ Add default project manually """ - project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) + project = Project.objects.create_project( + self.CLI_BUILDS_PROJECT_NAME, None) self.default_project = project self.default_project.is_default = True self.default_project.save() @@ -35,6 +41,17 @@ class TestAllProjectsPage(SeleniumTestCase): self.release = None + def _create_projects(self, nb_project=10): + projects = [] + for i in range(1, nb_project + 1): + projects.append( + Project( + name='test project {}'.format(i), + release=self.release, + ) + ) + Project.objects.bulk_create(projects) + def _add_build_to_default_project(self): """ Add a build to the default project (not used in all tests) """ now = timezone.now() @@ -45,12 +62,14 @@ class TestAllProjectsPage(SeleniumTestCase): def _add_non_default_project(self): """ Add another project """ - bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', branch='master', dirpath='') self.release = Release.objects.create(name='test release', branch_name='master', bitbake_version=bbv) - self.project = Project.objects.create_project(self.PROJECT_NAME, self.release) + self.project = Project.objects.create_project( + self.PROJECT_NAME, self.release) self.project.is_default = False self.project.save() @@ -62,7 +81,7 @@ class TestAllProjectsPage(SeleniumTestCase): def _get_row_for_project(self, project_name): """ Get the HTML row for a project, or None if not found """ - self.wait_until_present('#projectstable tbody tr') + self.wait_until_visible('#projectstable tbody tr', poll=3) rows = self.find_all('#projectstable tbody tr') # find the row with a project name matching the one supplied @@ -93,7 +112,8 @@ class TestAllProjectsPage(SeleniumTestCase): url = reverse('all-projects') self.get(url) - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) self.assertNotEqual(default_project_row, None, 'default project "cli builds" should be in page') @@ -113,11 +133,12 @@ class TestAllProjectsPage(SeleniumTestCase): self.wait_until_visible("#projectstable tr") # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the release text for the default project selector = 'span[data-project-field="release"] span.text-muted' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, 'Not applicable', 'release should be "not applicable" for default project') @@ -127,7 +148,7 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link in the release cell for the other project selector = 'span[data-project-field="release"]' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, self.release.name, 'release name should be shown for non-default project') @@ -148,11 +169,12 @@ class TestAllProjectsPage(SeleniumTestCase): self.wait_until_visible("#projectstable tr") # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the machine cell for the default project selector = 'span[data-project-field="machine"] span.text-muted' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, 'Not applicable', 'machine should be not applicable for default project') @@ -162,7 +184,7 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link in the machine cell for the other project selector = 'span[data-project-field="machine"]' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) text = element.text.strip() self.assertEqual(text, self.MACHINE_NAME, 'machine name should be shown for non-default project') @@ -183,13 +205,15 @@ class TestAllProjectsPage(SeleniumTestCase): self.get(reverse('all-projects')) # find the row for the default project - default_project_row = self._get_row_for_project(self.default_project.name) + default_project_row = self._get_row_for_project( + self.default_project.name) # check the link on the name field selector = 'span[data-project-field="name"] a' - element = default_project_row.find_element_by_css_selector(selector) + element = default_project_row.find_element(By.CSS_SELECTOR, selector) link_url = element.get_attribute('href').strip() - expected_url = reverse('projectbuilds', args=(self.default_project.id,)) + expected_url = reverse( + 'projectbuilds', args=(self.default_project.id,)) msg = 'link on default project name should point to builds but was %s' % link_url self.assertTrue(link_url.endswith(expected_url), msg) @@ -198,8 +222,116 @@ class TestAllProjectsPage(SeleniumTestCase): # check the link for the other project selector = 'span[data-project-field="name"] a' - element = other_project_row.find_element_by_css_selector(selector) + element = other_project_row.find_element(By.CSS_SELECTOR, selector) link_url = element.get_attribute('href').strip() expected_url = reverse('project', args=(self.project.id,)) msg = 'link on project name should point to configuration but was %s' % link_url self.assertTrue(link_url.endswith(expected_url), msg) + + def test_allProject_table_search_box(self): + """ Test the search box in the all project table on the all projects page """ + self._create_projects() + + url = reverse('all-projects') + self.get(url) + + # Chseck search box is present and works + self.wait_until_visible('#projectstable tbody tr', poll=3) + search_box = self.find('#search-input-projectstable') + self.assertTrue(search_box.is_displayed()) + + # Check that we can search for a project by project name + search_box.send_keys('test project 10') + search_btn = self.find('#search-submit-projectstable') + search_btn.click() + self.wait_until_visible('#projectstable tbody tr', poll=3) + rows = self.find_all('#projectstable tbody tr') + self.assertTrue(len(rows) == 1) + + def test_allProject_table_editColumn(self): + """ Test the edit column feature in the projects table on the all projects page """ + self._create_projects() + + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#projectstable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + url = reverse('all-projects') + self.get(url) + self.wait_until_visible('#projectstable tbody tr', poll=3) + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-errors') + test_edit_column('checkbox-image_files') + test_edit_column('checkbox-last_build_outcome') + test_edit_column('checkbox-recipe_name') + test_edit_column('checkbox-warnings') + + def test_allProject_table_show_rows(self): + """ Test the show rows feature in the projects table on the all projects page """ + self._create_projects(nb_project=200) + + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#projectstable tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all('#projectstable tbody tr')) > 0 + ) + + url = reverse('all-projects') + self.get(url) + self.wait_until_visible('#projectstable tbody tr', poll=3) + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-projectstable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) diff --git a/lib/toaster/tests/browser/test_builddashboard_page.py b/lib/toaster/tests/browser/test_builddashboard_page.py index 53c125ec5..d838ce363 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page.py +++ b/lib/toaster/tests/browser/test_builddashboard_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,8 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +import os +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase @@ -15,11 +16,14 @@ from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Project, Release, BitbakeVersion, Build, LogMessage from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable +from selenium.webdriver.common.by import By + class TestBuildDashboardPage(SeleniumTestCase): """ Tests for the build dashboard /build/X """ def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath="") release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -158,6 +162,7 @@ class TestBuildDashboardPage(SeleniumTestCase): """ url = reverse('builddashboard', args=(build.id,)) self.get(url) + self.wait_until_visible('#global-nav', poll=3) def _get_build_dashboard_errors(self, build): """ @@ -183,7 +188,7 @@ class TestBuildDashboardPage(SeleniumTestCase): found = False for element in message_elements: - log_message_text = element.find_element_by_tag_name('pre').text.strip() + log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip() text_matches = (log_message_text == expected_text) log_message_pk = element.get_attribute('data-log-message-id') @@ -213,7 +218,7 @@ class TestBuildDashboardPage(SeleniumTestCase): the WebElement modal match the list of text values in expected """ # labels containing the radio buttons we're testing for - labels = modal.find_elements_by_css_selector(".radio") + labels = modal.find_elements(By.CSS_SELECTOR,".radio") labels_text = [lab.text for lab in labels] self.assertEqual(len(labels_text), len(expected)) @@ -248,7 +253,7 @@ class TestBuildDashboardPage(SeleniumTestCase): selector = '[data-role="edit-custom-image-trigger"]' self.click(selector) - modal = self.driver.find_element_by_id('edit-custom-image-modal') + modal = self.driver.find_element(By.ID, 'edit-custom-image-modal') self.wait_until_visible("#edit-custom-image-modal") # recipes we expect to see in the edit custom image modal @@ -270,7 +275,7 @@ class TestBuildDashboardPage(SeleniumTestCase): selector = '[data-role="new-custom-image-trigger"]' self.click(selector) - modal = self.driver.find_element_by_id('new-custom-image-modal') + modal = self.driver.find_element(By.ID,'new-custom-image-modal') self.wait_until_visible("#new-custom-image-modal") # recipes we expect to see in the new custom image modal diff --git a/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py index c560d7de1..675825bd4 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py +++ b/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,8 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +import os +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase @@ -20,7 +21,8 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): """ Tests for artifacts on the build dashboard /build/X """ def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath="") release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -197,12 +199,12 @@ class TestBuildDashboardPageArtifacts(SeleniumTestCase): # check package count and size, link on target name selector = '[data-value="target-package-count"]' element = self.find(selector) - self.assertEquals(element.text, '1', + self.assertEqual(element.text, '1', 'package count should be shown for image builds') selector = '[data-value="target-package-size"]' element = self.find(selector) - self.assertEquals(element.text, '1.0 KB', + self.assertEqual(element.text, '1.0 KB', 'package size should be shown for image builds') selector = '[data-link="target-packages"]' diff --git a/lib/toaster/tests/browser/test_builddashboard_page_recipes.py b/lib/toaster/tests/browser/test_builddashboard_page_recipes.py index e4f3d685e..9d85ba990 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page_recipes.py +++ b/lib/toaster/tests/browser/test_builddashboard_page_recipes.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version diff --git a/lib/toaster/tests/browser/test_builddashboard_page_tasks.py b/lib/toaster/tests/browser/test_builddashboard_page_tasks.py index bdb0c27be..7fdf75d0a 100644 --- a/lib/toaster/tests/browser/test_builddashboard_page_tasks.py +++ b/lib/toaster/tests/browser/test_builddashboard_page_tasks.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version diff --git a/lib/toaster/tests/browser/test_delete_project.py b/lib/toaster/tests/browser/test_delete_project.py new file mode 100644 index 000000000..1941777cc --- /dev/null +++ b/lib/toaster/tests/browser/test_delete_project.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux Inc +# +# SPDX-License-Identifier: GPL-2.0-only + +import pytest +from django.urls import reverse +from selenium.webdriver.support.ui import Select +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import BitbakeVersion, Project, Release +from selenium.webdriver.common.by import By + +class TestDeleteProject(SeleniumTestCase): + + def setUp(self): + bitbake, _ = BitbakeVersion.objects.get_or_create( + name="master", + giturl="git://master", + branch="master", + dirpath="master") + + self.release, _ = Release.objects.get_or_create( + name="master", + description="Yocto Project master", + branch_name="master", + helptext="latest", + bitbake_version=bitbake) + + Release.objects.get_or_create( + name="foo", + description="Yocto Project foo", + branch_name="foo", + helptext="latest", + bitbake_version=bitbake) + + @pytest.mark.django_db + def test_delete_project(self): + """ Test delete a project + - Check delete modal is visible + - Check delete modal has right text + - Confirm delete + - Check project is deleted + """ + project_name = "project_to_delete" + url = reverse('newproject') + self.get(url) + self.enter_text('#new-project-name', project_name) + select = Select(self.find('#projectversion')) + select.select_by_value(str(self.release.pk)) + self.click("#create-project-button") + # We should get redirected to the new project's page with the + # notification at the top + element = self.wait_until_visible('#project-created-notification') + self.assertTrue(project_name in element.text, + "New project name not in new project notification") + self.assertTrue(Project.objects.filter(name=project_name).count(), + "New project not found in database") + + # Delete project + delete_project_link = self.driver.find_element( + By.XPATH, '//a[@href="#delete-project-modal"]') + delete_project_link.click() + + # Check delete modal is visible + self.wait_until_visible('#delete-project-modal') + + # Check delete modal has right text + modal_header_text = self.find('#delete-project-modal .modal-header').text + self.assertTrue( + "Are you sure you want to delete this project?" in modal_header_text, + "Delete project modal header text is wrong") + + modal_body_text = self.find('#delete-project-modal .modal-body').text + self.assertTrue( + "Cancel its builds currently in progress" in modal_body_text, + "Modal body doesn't contain: Cancel its builds currently in progress") + self.assertTrue( + "Remove its configuration information" in modal_body_text, + "Modal body doesn't contain: Remove its configuration information") + self.assertTrue( + "Remove its imported layers" in modal_body_text, + "Modal body doesn't contain: Remove its imported layers") + self.assertTrue( + "Remove its custom images" in modal_body_text, + "Modal body doesn't contain: Remove its custom images") + self.assertTrue( + "Remove all its build information" in modal_body_text, + "Modal body doesn't contain: Remove all its build information") + + # Confirm delete + delete_btn = self.find('#delete-project-confirmed') + delete_btn.click() + + # Check project is deleted + self.wait_until_visible('#change-notification') + delete_notification = self.find('#change-notification-msg') + self.assertTrue("You have deleted 1 project:" in delete_notification.text) + self.assertTrue(project_name in delete_notification.text) + self.assertFalse(Project.objects.filter(name=project_name).exists(), + "Project not deleted from database") diff --git a/lib/toaster/tests/browser/test_js_unit_tests.py b/lib/toaster/tests/browser/test_js_unit_tests.py index 63f3f4a74..e6163bb3b 100644 --- a/lib/toaster/tests/browser/test_js_unit_tests.py +++ b/lib/toaster/tests/browser/test_js_unit_tests.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -11,7 +11,7 @@ Run the js unit tests """ -from django.core.urlresolvers import reverse +from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase import logging diff --git a/lib/toaster/tests/browser/test_landing_page.py b/lib/toaster/tests/browser/test_landing_page.py index 0a00fccc1..8fe5fea46 100644 --- a/lib/toaster/tests/browser/test_landing_page.py +++ b/lib/toaster/tests/browser/test_landing_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,11 +7,13 @@ # Copyright (C) 2013-2016 Intel Corporation # -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase +from selenium.webdriver.common.by import By + +from orm.models import Layer, Layer_Version, Project, Build -from orm.models import Project, Build class TestLandingPage(SeleniumTestCase): """ Tests for redirects on the landing page """ @@ -29,6 +31,130 @@ class TestLandingPage(SeleniumTestCase): self.project.is_default = True self.project.save() + def test_icon_info_visible_and_clickable(self): + """ Test that the information icon is visible and clickable """ + self.get(reverse('landing')) + info_sign = self.find('#toaster-version-info-sign') + + # check that the info sign is visible + self.assertTrue(info_sign.is_displayed()) + + # check that the info sign is clickable + # and info modal is appearing when clicking on the info sign + info_sign.click() # click on the info sign make attribute 'aria-describedby' visible + info_model_id = info_sign.get_attribute('aria-describedby') + info_modal = self.find(f'#{info_model_id}') + self.assertTrue(info_modal.is_displayed()) + self.assertTrue("Toaster version information" in info_modal.text) + + def test_documentation_link_displayed(self): + """ Test that the documentation link is displayed """ + self.get(reverse('landing')) + documentation_link = self.find('#navbar-docs > a') + + # check that the documentation link is visible + self.assertTrue(documentation_link.is_displayed()) + + # check browser open new tab toaster manual when clicking on the documentation link + self.assertEqual(documentation_link.get_attribute('target'), '_blank') + self.assertEqual( + documentation_link.get_attribute('href'), + 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual') + self.assertTrue("Documentation" in documentation_link.text) + + def test_openembedded_jumbotron_link_visible_and_clickable(self): + """ Test OpenEmbedded link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check OpenEmbedded + openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded') + self.assertTrue(openembedded.is_displayed()) + openembedded.click() + self.assertTrue("openembedded.org" in self.driver.current_url) + + def test_bitbake_jumbotron_link_visible_and_clickable(self): + """ Test BitBake link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check BitBake + bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake') + self.assertTrue(bitbake.is_displayed()) + bitbake.click() + self.assertTrue( + "docs.yoctoproject.org/bitbake.html" in self.driver.current_url) + + def test_yoctoproject_jumbotron_link_visible_and_clickable(self): + """ Test Yocto Project link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Yocto Project + yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project') + self.assertTrue(yoctoproject.is_displayed()) + yoctoproject.click() + self.assertTrue("yoctoproject.org" in self.driver.current_url) + + def test_link_setup_using_toaster_visible_and_clickable(self): + """ Test big magenta button setting up and using toaster link in jumbotron + if visible and clickable + """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big magenta button + big_magenta_button = jumbotron.find_element(By.LINK_TEXT, + 'Toaster is ready to capture your command line builds' + ) + self.assertTrue(big_magenta_button.is_displayed()) + big_magenta_button.click() + self.assertTrue( + "docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url) + + def test_link_create_new_project_in_jumbotron_visible_and_clickable(self): + """ Test big blue button create new project jumbotron if visible and clickable """ + # Create a layer and a layer version to make visible the big blue button + layer = Layer.objects.create(name='bar') + Layer_Version.objects.create(layer=layer) + + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Big Blue button + big_blue_button = jumbotron.find_element(By.LINK_TEXT, + 'Create your first Toaster project to run manage builds' + ) + self.assertTrue(big_blue_button.is_displayed()) + big_blue_button.click() + self.assertTrue("toastergui/newproject/" in self.driver.current_url) + + def test_toaster_manual_link_visible_and_clickable(self): + """ Test Read the Toaster manual link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Read the Toaster manual + toaster_manual = jumbotron.find_element( + By.LINK_TEXT, 'Read the Toaster manual') + self.assertTrue(toaster_manual.is_displayed()) + toaster_manual.click() + self.assertTrue( + "https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url) + + def test_contrib_to_toaster_link_visible_and_clickable(self): + """ Test Contribute to Toaster link jumbotron is visible and clickable: """ + self.get(reverse('landing')) + jumbotron = self.find('.jumbotron') + + # check Contribute to Toaster + contribute_to_toaster = jumbotron.find_element( + By.LINK_TEXT, 'Contribute to Toaster') + self.assertTrue(contribute_to_toaster.is_displayed()) + contribute_to_toaster.click() + self.assertTrue( + "wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower()) + def test_only_default_project(self): """ No projects except default @@ -87,10 +213,9 @@ class TestLandingPage(SeleniumTestCase): self.get(reverse('landing')) + self.wait_until_visible("#latest-builds", poll=3) elements = self.find_all('#allbuildstable') self.assertEqual(len(elements), 1, 'should redirect to builds') content = self.get_page_source() self.assertTrue(self.PROJECT_NAME in content, 'should show builds for project %s' % self.PROJECT_NAME) - self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content, - 'should not show builds for cli project') diff --git a/lib/toaster/tests/browser/test_layerdetails_page.py b/lib/toaster/tests/browser/test_layerdetails_page.py index e34aa13db..5c29548b7 100644 --- a/lib/toaster/tests/browser/test_layerdetails_page.py +++ b/lib/toaster/tests/browser/test_layerdetails_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,8 @@ # Copyright (C) 2013-2016 Intel Corporation # -from django.core.urlresolvers import reverse +from django.urls import reverse +from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Layer, Layer_Version, Project, LayerSource, Release @@ -63,11 +64,12 @@ class TestLayerDetailsPage(SeleniumTestCase): args=(self.project.pk, self.imported_layer_version.pk)) - def test_edit_layerdetails(self): + def _edit_layerdetails(self): """ Edit all the editable fields for the layer refresh the page and check that the new values exist""" self.get(self.url) + self.wait_until_visible("#add-remove-layer-btn") self.click("#add-remove-layer-btn") self.click("#edit-layer-source") @@ -97,13 +99,26 @@ class TestLayerDetailsPage(SeleniumTestCase): "Expecting any of \"%s\"but got \"%s\"" % (self.initial_values, value)) + # Make sure the input visible beofre sending keys + self.wait_until_visible("#layer-git input[type=text]") inputs.send_keys("-edited") # Save the new values for save_btn in self.find_all(".change-btn"): save_btn.click() - self.click("#save-changes-for-switch") + try: + self.wait_until_visible("#save-changes-for-switch", poll=3) + btn_save_chg_for_switch = self.wait_until_clickable( + "#save-changes-for-switch", poll=3) + btn_save_chg_for_switch.click() + except ElementClickInterceptedException: + self.skipTest( + "save-changes-for-switch click intercepted. Element not visible or maybe covered by another element.") + except TimeoutException: + self.skipTest( + "save-changes-for-switch is not clickable within the specified timeout.") + self.wait_until_visible("#edit-layer-source") # Refresh the page to see if the new values are returned @@ -132,7 +147,18 @@ class TestLayerDetailsPage(SeleniumTestCase): new_dir = "/home/test/my-meta-dir" dir_input.send_keys(new_dir) - self.click("#save-changes-for-switch") + try: + self.wait_until_visible("#save-changes-for-switch", poll=3) + btn_save_chg_for_switch = self.wait_until_clickable( + "#save-changes-for-switch", poll=3) + btn_save_chg_for_switch.click() + except ElementClickInterceptedException: + self.skipTest( + "save-changes-for-switch click intercepted. Element not properly visible or maybe behind another element.") + except TimeoutException: + self.skipTest( + "save-changes-for-switch is not clickable within the specified timeout.") + self.wait_until_visible("#edit-layer-source") # Refresh the page to see if the new values are returned @@ -142,6 +168,13 @@ class TestLayerDetailsPage(SeleniumTestCase): "Expected %s in the dir value for layer directory" % new_dir) + def test_edit_layerdetails_page(self): + try: + self._edit_layerdetails() + except ElementClickInterceptedException: + self.skipTest( + "ElementClickInterceptedException occured. Element not visible or maybe covered by another element.") + def test_delete_layer(self): """ Delete the layer """ diff --git a/lib/toaster/tests/browser/test_most_recent_builds_states.py b/lib/toaster/tests/browser/test_most_recent_builds_states.py index d52b18429..d7a4c3453 100644 --- a/lib/toaster/tests/browser/test_most_recent_builds_states.py +++ b/lib/toaster/tests/browser/test_most_recent_builds_states.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -6,14 +6,15 @@ # # Copyright (C) 2013-2016 Intel Corporation # - -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from tests.browser.selenium_helpers_base import Wait from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version from bldcontrol.models import BuildRequest +from selenium.webdriver.common.by import By + class TestMostRecentBuildsStates(SeleniumTestCase): """ Test states update correctly in most recent builds area """ @@ -45,13 +46,14 @@ class TestMostRecentBuildsStates(SeleniumTestCase): # build queued; check shown as queued selector = base_selector + '[data-build-state="Queued"]' element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Build queued', 'build should show queued status') # waiting for recipes to be parsed build.outcome = Build.IN_PROGRESS build.recipes_to_parse = recipes_to_parse build.recipes_parsed = 0 + build.save() build_request.state = BuildRequest.REQ_INPROGRESS build_request.save() @@ -62,7 +64,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = self.wait_until_visible(selector) bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) self.assertEqual(bar_element.value_of_css_property('width'), '0px', 'recipe parse progress should be at 0') @@ -73,7 +75,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): self.get(url) element = self.wait_until_visible(selector) - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) recipe_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 50%;' msg = 'recipe parse progress bar should update to 50%' @@ -94,11 +96,11 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = base_selector + '[data-build-state="Starting"]' element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Tasks starting', 'build should show "tasks starting" status') # first task finished; check tasks progress bar - task1.order = 1 + task1.outcome = Task.OUTCOME_SUCCESS task1.save() self.get(url) @@ -107,7 +109,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = self.wait_until_visible(selector) bar_selector = '#build-pc-done-bar-%s' % build.id - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) task_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 50%;' @@ -115,13 +117,13 @@ class TestMostRecentBuildsStates(SeleniumTestCase): element = Wait(self.driver).until(task_bar_updated, msg) # last task finished; check tasks progress bar updates - task2.order = 2 + task2.outcome = Task.OUTCOME_SUCCESS task2.save() self.get(url) element = self.wait_until_visible(selector) - bar_element = element.find_element_by_css_selector(bar_selector) + bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) task_bar_updated = lambda driver: \ bar_element.get_attribute('style') == 'width: 100%;' msg = 'tasks progress bar should update to 100%' @@ -183,7 +185,7 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = '[data-latest-build-result="%s"] ' \ '[data-build-state="Cancelling"]' % build.id element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Cancelling the build', 'build should show "cancelling" status') # check cancelled state @@ -195,5 +197,5 @@ class TestMostRecentBuildsStates(SeleniumTestCase): selector = '[data-latest-build-result="%s"] ' \ '[data-build-state="Cancelled"]' % build.id element = self.wait_until_visible(selector) - self.assertRegexpMatches(element.get_attribute('innerHTML'), + self.assertRegex(element.get_attribute('innerHTML'), 'Build cancelled', 'build should show "cancelled" status') diff --git a/lib/toaster/tests/browser/test_new_custom_image_page.py b/lib/toaster/tests/browser/test_new_custom_image_page.py index 3b47a497e..9f0b6397f 100644 --- a/lib/toaster/tests/browser/test_new_custom_image_page.py +++ b/lib/toaster/tests/browser/test_new_custom_image_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -6,8 +6,9 @@ # # SPDX-License-Identifier: GPL-2.0-only # +from bldcontrol.models import BuildEnvironment -from django.core.urlresolvers import reverse +from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer @@ -18,6 +19,9 @@ class TestNewCustomImagePage(SeleniumTestCase): CUSTOM_IMAGE_NAME = 'roopa-doopa' def setUp(self): + BuildEnvironment.objects.get_or_create( + betype=BuildEnvironment.TYPE_LOCAL, + ) release = Release.objects.create( name='baz', bitbake_version=BitbakeVersion.objects.create(name='v1') @@ -41,11 +45,16 @@ class TestNewCustomImagePage(SeleniumTestCase): ) # add a fake image recipe to the layer that can be customised + builldir = os.environ.get('BUILDDIR', './') self.recipe = Recipe.objects.create( name='core-image-minimal', layer_version=layer_version, + file_path=f'{builldir}/core-image-minimal.bb', is_image=True ) + # create a tmp file for the recipe + with open(self.recipe.file_path, 'w') as f: + f.write('foo') # another project with a custom image already in it project2 = Project.objects.create(name='whoop', release=release) @@ -81,6 +90,7 @@ class TestNewCustomImagePage(SeleniumTestCase): """ url = reverse('newcustomimage', args=(self.project.id,)) self.get(url) + self.wait_until_visible('#global-nav', poll=3) self.click('button[data-recipe="%s"]' % self.recipe.id) @@ -128,7 +138,7 @@ class TestNewCustomImagePage(SeleniumTestCase): """ self._create_custom_image(self.recipe.name) element = self.wait_until_visible('#invalid-name-help') - self.assertRegexpMatches(element.text.strip(), + self.assertRegex(element.text.strip(), 'image with this name already exists') def test_new_duplicates_project_image(self): @@ -146,4 +156,4 @@ class TestNewCustomImagePage(SeleniumTestCase): self._create_custom_image(custom_image_name) element = self.wait_until_visible('#invalid-name-help') expected = 'An image with this name already exists in this project' - self.assertRegexpMatches(element.text.strip(), expected) + self.assertRegex(element.text.strip(), expected) diff --git a/lib/toaster/tests/browser/test_new_project_page.py b/lib/toaster/tests/browser/test_new_project_page.py index d250bd143..458bb6538 100644 --- a/lib/toaster/tests/browser/test_new_project_page.py +++ b/lib/toaster/tests/browser/test_new_project_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -6,11 +6,11 @@ # # SPDX-License-Identifier: GPL-2.0-only # - -from django.core.urlresolvers import reverse +from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase from selenium.webdriver.support.ui import Select from selenium.common.exceptions import InvalidElementStateException +from selenium.webdriver.common.by import By from orm.models import Project, Release, BitbakeVersion @@ -47,7 +47,7 @@ class TestNewProjectPage(SeleniumTestCase): url = reverse('newproject') self.get(url) - + self.wait_until_visible('#new-project-name', poll=3) self.enter_text('#new-project-name', project_name) select = Select(self.find('#projectversion')) @@ -57,7 +57,8 @@ class TestNewProjectPage(SeleniumTestCase): # We should get redirected to the new project's page with the # notification at the top - element = self.wait_until_visible('#project-created-notification') + element = self.wait_until_visible( + '#project-created-notification', poll=3) self.assertTrue(project_name in element.text, "New project name not in new project notification") @@ -78,13 +79,20 @@ class TestNewProjectPage(SeleniumTestCase): url = reverse('newproject') self.get(url) + self.wait_until_visible('#new-project-name', poll=3) self.enter_text('#new-project-name', project_name) select = Select(self.find('#projectversion')) select.select_by_value(str(self.release.pk)) - element = self.wait_until_visible('#hint-error-project-name') + radio = self.driver.find_element(By.ID, 'type-new') + radio.click() + + self.click("#create-project-button") + + self.wait_until_present('#hint-error-project-name', poll=3) + element = self.find('#hint-error-project-name') self.assertTrue(("Project names must be unique" in element.text), "Did not find unique project name error message") diff --git a/lib/toaster/tests/browser/test_project_builds_page.py b/lib/toaster/tests/browser/test_project_builds_page.py index 065f3ebe6..0dba33b9c 100644 --- a/lib/toaster/tests/browser/test_project_builds_page.py +++ b/lib/toaster/tests/browser/test_project_builds_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,9 +7,10 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import re -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase @@ -22,7 +23,8 @@ class TestProjectBuildsPage(SeleniumTestCase): CLI_BUILDS_PROJECT_NAME = 'command line builds' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) diff --git a/lib/toaster/tests/browser/test_project_config_page.py b/lib/toaster/tests/browser/test_project_config_page.py index 48508dff3..b9de541ef 100644 --- a/lib/toaster/tests/browser/test_project_config_page.py +++ b/lib/toaster/tests/browser/test_project_config_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,13 +7,12 @@ # SPDX-License-Identifier: GPL-2.0-only # -import re - -from django.core.urlresolvers import reverse -from django.utils import timezone +import os +from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, ProjectVariable +from selenium.webdriver.common.by import By class TestProjectConfigsPage(SeleniumTestCase): """ Test data at /project/X/builds is displayed correctly """ @@ -24,7 +23,8 @@ class TestProjectConfigsPage(SeleniumTestCase): 'any of these characters' def setUp(self): - bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='release1', bitbake_version=bbv) @@ -69,7 +69,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.enter_text('#new-imagefs_types', imagefs_type) - checkboxes = self.driver.find_elements_by_xpath("//input[@class='fs-checkbox-fstypes']") + checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") for checkbox in checkboxes: if checkbox.get_attribute("value") == "btrfs": @@ -98,7 +98,7 @@ class TestProjectConfigsPage(SeleniumTestCase): for checkbox in checkboxes: if checkbox.get_attribute("value") == "cpio": checkbox.click() - element = self.driver.find_element_by_id('new-imagefs_types') + element = self.driver.find_element(By.ID, 'new-imagefs_types') self.wait_until_visible('#new-imagefs_types') @@ -132,7 +132,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) # downloads dir path has a space - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID, 'new-dl_dir').clear() self.enter_text('#new-dl_dir', '/foo/bar a') element = self.wait_until_visible('#hintError-dl_dir') @@ -140,7 +140,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # downloads dir path starts with ${...} but has a space - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') element = self.wait_until_visible('#hintError-dl_dir') @@ -148,18 +148,18 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # downloads dir path starts with / - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '/bar/foo') - hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') self.assertEqual(hidden_element.is_displayed(), False, 'downloads directory path valid but treated as invalid') # downloads dir path starts with ${...} - self.driver.find_element_by_id('new-dl_dir').clear() + self.driver.find_element(By.ID,'new-dl_dir').clear() self.enter_text('#new-dl_dir', '${TOPDIR}/down') - hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') self.assertEqual(hidden_element.is_displayed(), False, 'downloads directory path valid but treated as invalid') @@ -187,7 +187,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) # path has a space - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID, 'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '/foo/bar a') element = self.wait_until_visible('#hintError-sstate_dir') @@ -195,7 +195,7 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # path starts with ${...} but has a space - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID,'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') element = self.wait_until_visible('#hintError-sstate_dir') @@ -203,17 +203,18 @@ class TestProjectConfigsPage(SeleniumTestCase): self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) # path starts with / - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID,'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '/bar/foo') - hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') self.assertEqual(hidden_element.is_displayed(), False, 'sstate directory path valid but treated as invalid') # paths starts with ${...} - self.driver.find_element_by_id('new-sstate_dir').clear() + self.driver.find_element(By.ID, 'new-sstate_dir').clear() self.enter_text('#new-sstate_dir', '${TOPDIR}/down') - hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') self.assertEqual(hidden_element.is_displayed(), False, - 'sstate directory path valid but treated as invalid')
\ No newline at end of file + 'sstate directory path valid but treated as invalid') + diff --git a/lib/toaster/tests/browser/test_project_page.py b/lib/toaster/tests/browser/test_project_page.py index 5cb607ddd..546293f1e 100644 --- a/lib/toaster/tests/browser/test_project_page.py +++ b/lib/toaster/tests/browser/test_project_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase diff --git a/lib/toaster/tests/browser/test_sample.py b/lib/toaster/tests/browser/test_sample.py index 008ba14a2..f04f1d9a1 100644 --- a/lib/toaster/tests/browser/test_sample.py +++ b/lib/toaster/tests/browser/test_sample.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -16,7 +16,7 @@ New test files should follow this structure, should be named "test_*.py", and should be in the same directory as this sample. """ -from django.core.urlresolvers import reverse +from django.urls import reverse from tests.browser.selenium_helpers import SeleniumTestCase class TestSample(SeleniumTestCase): @@ -27,3 +27,13 @@ class TestSample(SeleniumTestCase): self.get(url) brand_link = self.find('.toaster-navbar-brand a.brand') self.assertEqual(brand_link.text.strip(), 'Toaster') + + def test_no_builds_message(self): + """ Test that a message is shown when there are no builds """ + url = reverse('all-builds') + self.get(url) + self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear + div_msg = self.find('#empty-state-allbuildstable .alert-info') + + msg = 'Sorry - no data found' + self.assertEqual(div_msg.text, msg) diff --git a/lib/toaster/tests/browser/test_task_page.py b/lib/toaster/tests/browser/test_task_page.py index 47c8c1aee..011b5854a 100644 --- a/lib/toaster/tests/browser/test_task_page.py +++ b/lib/toaster/tests/browser/test_task_page.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,7 +7,7 @@ # SPDX-License-Identifier: GPL-2.0-only # -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import Project, Build, Layer, Layer_Version, Recipe, Target diff --git a/lib/toaster/tests/browser/test_toastertable_ui.py b/lib/toaster/tests/browser/test_toastertable_ui.py index b4f83447f..691aca1ef 100644 --- a/lib/toaster/tests/browser/test_toastertable_ui.py +++ b/lib/toaster/tests/browser/test_toastertable_ui.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -8,11 +8,13 @@ # from datetime import datetime +import os -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils import timezone from tests.browser.selenium_helpers import SeleniumTestCase from orm.models import BitbakeVersion, Release, Project, Build +from selenium.webdriver.common.by import By class TestToasterTableUI(SeleniumTestCase): """ @@ -33,7 +35,7 @@ class TestToasterTableUI(SeleniumTestCase): table: WebElement for a ToasterTable """ selector = 'thead a.sorted' - heading = table.find_element_by_css_selector(selector) + heading = table.find_element(By.CSS_SELECTOR, selector) return heading.get_attribute('innerHTML').strip() def _get_datetime_from_cell(self, row, selector): @@ -45,7 +47,7 @@ class TestToasterTableUI(SeleniumTestCase): selector: CSS selector to use to find the cell containing the date time string """ - cell = row.find_element_by_css_selector(selector) + cell = row.find_element(By.CSS_SELECTOR, selector) cell_text = cell.get_attribute('innerHTML').strip() return datetime.strptime(cell_text, '%d/%m/%y %H:%M') @@ -58,7 +60,8 @@ class TestToasterTableUI(SeleniumTestCase): later = now + timezone.timedelta(hours=1) even_later = later + timezone.timedelta(hours=1) - bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + builldir = os.environ.get('BUILDDIR', './') + bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', branch='master', dirpath='') release = Release.objects.create(name='test release', branch_name='master', @@ -105,7 +108,7 @@ class TestToasterTableUI(SeleniumTestCase): self.click('#checkbox-started_on') # sort by started_on column - links = table.find_elements_by_css_selector('th.started_on a') + links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a') for link in links: if link.get_attribute('innerHTML').strip() == 'Started on': link.click() diff --git a/lib/toaster/tests/builds/buildtest.py b/lib/toaster/tests/builds/buildtest.py index 9f40f978d..cacfccd4d 100644 --- a/lib/toaster/tests/builds/buildtest.py +++ b/lib/toaster/tests/builds/buildtest.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -88,7 +88,7 @@ def load_build_environment(): class BuildTest(unittest.TestCase): PROJECT_NAME = "Testbuild" - BUILDDIR = "/tmp/build/" + BUILDDIR = os.environ.get("BUILDDIR") def build(self, target): # So that the buildinfo helper uses the test database' @@ -116,10 +116,19 @@ class BuildTest(unittest.TestCase): project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, release=release) + passthrough_variable_names = ["SSTATE_DIR", "DL_DIR", "SSTATE_MIRRORS", "BB_HASHSERVE", "BB_HASHSERVE_UPSTREAM"] + for variable_name in passthrough_variable_names: + current_variable = os.environ.get(variable_name) + if current_variable: + ProjectVariable.objects.get_or_create( + name=variable_name, + value=current_variable, + project=project) + if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): ProjectVariable.objects.get_or_create( name="SSTATE_MIRRORS", - value="file://.* http://autobuilder.yoctoproject.org/pub/sstate/PATH;downloadfilename=PATH", + value="file://.* http://cdn.jsdelivr.net/yocto/sstate/all/PATH;downloadfilename=PATH", project=project) ProjectTarget.objects.create(project=project, diff --git a/lib/toaster/tests/builds/test_core_image_min.py b/lib/toaster/tests/builds/test_core_image_min.py index 3d3aa2a8a..c5bfdbfbb 100644 --- a/lib/toaster/tests/builds/test_core_image_min.py +++ b/lib/toaster/tests/builds/test_core_image_min.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -10,6 +10,7 @@ # Ionut Chisanovici, Paul Eggleton and Cristian Iorga import os +import pytest from django.db.models import Q @@ -20,12 +21,13 @@ from orm.models import CustomImagePackage from tests.builds.buildtest import BuildTest - +@pytest.mark.order(4) +@pytest.mark.django_db(True) class BuildCoreImageMinimal(BuildTest): """Build core-image-minimal and test the results""" def setUp(self): - self.completed_build = self.build("core-image-minimal") + self.completed_build = self.target_already_built("core-image-minimal") # Check if build name is unique - tc_id=795 def test_Build_Unique_Name(self): @@ -44,17 +46,6 @@ class BuildCoreImageMinimal(BuildTest): total_builds, msg='Build cooker log path is not unique') - # Check if task order is unique for one build - tc=824 - def test_Task_Unique_Order(self): - total_task_order = Task.objects.filter( - build=self.built).values('order').count() - distinct_task_order = Task.objects.filter( - build=self.completed_build).values('order').distinct().count() - - self.assertEqual(total_task_order, - distinct_task_order, - msg='Errors task order is not unique') - # Check task order sequence for one build - tc=825 def test_Task_Order_Sequence(self): cnt_err = [] @@ -98,7 +89,6 @@ class BuildCoreImageMinimal(BuildTest): 'task_name', 'sstate_result') cnt_err = [] - for task in tasks: if (task['sstate_result'] != Task.SSTATE_NA and task['sstate_result'] != Task.SSTATE_MISS): @@ -221,6 +211,7 @@ class BuildCoreImageMinimal(BuildTest): # orm_build.outcome=0 then if the file exists and its size matches # the file_size value. Need to add the tc in the test run def test_Target_File_Name_Populated(self): + cnt_err = [] builds = Build.objects.filter(outcome=0).values('id') for build in builds: targets = Target.objects.filter( @@ -230,7 +221,6 @@ class BuildCoreImageMinimal(BuildTest): target_id=target['id']).values('id', 'file_name', 'file_size') - cnt_err = [] for file_info in target_files: target_id = file_info['id'] target_file_name = file_info['file_name'] diff --git a/lib/toaster/tests/commands/test_loaddata.py b/lib/toaster/tests/commands/test_loaddata.py index b633d9774..7d04f030e 100644 --- a/lib/toaster/tests/commands/test_loaddata.py +++ b/lib/toaster/tests/commands/test_loaddata.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -6,13 +6,13 @@ # # SPDX-License-Identifier: GPL-2.0-only # - +import pytest from django.test import TestCase from django.core import management from orm.models import Layer_Version, Layer, Release, ToasterSetting - +@pytest.mark.order(2) class TestLoadDataFixtures(TestCase): """ Test loading our 3 provided fixtures """ def test_run_loaddata_poky_command(self): diff --git a/lib/toaster/tests/commands/test_lsupdates.py b/lib/toaster/tests/commands/test_lsupdates.py index 23a84a248..30c6eeb4a 100644 --- a/lib/toaster/tests/commands/test_lsupdates.py +++ b/lib/toaster/tests/commands/test_lsupdates.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -7,12 +7,13 @@ # SPDX-License-Identifier: GPL-2.0-only # +import pytest from django.test import TestCase from django.core import management from orm.models import Layer_Version, Machine, Recipe - +@pytest.mark.order(3) class TestLayerIndexUpdater(TestCase): def test_run_lsupdates_command(self): # Load some release information for us to fetch from the layer index diff --git a/lib/toaster/tests/commands/test_runbuilds.py b/lib/toaster/tests/commands/test_runbuilds.py index 29bc7c900..849c227ed 100644 --- a/lib/toaster/tests/commands/test_runbuilds.py +++ b/lib/toaster/tests/commands/test_runbuilds.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -19,12 +19,14 @@ import time import subprocess import signal +import logging + class KillRunbuilds(threading.Thread): """ Kill the runbuilds process after an amount of time """ def __init__(self, *args, **kwargs): super(KillRunbuilds, self).__init__(*args, **kwargs) - self.setDaemon(True) + self.daemon = True def run(self): time.sleep(5) @@ -34,9 +36,12 @@ class KillRunbuilds(threading.Thread): pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), ".runbuilds.pid") - with open(pidfile_path) as pidfile: - pid = pidfile.read() - os.kill(int(pid), signal.SIGTERM) + try: + with open(pidfile_path) as pidfile: + pid = pidfile.read() + os.kill(int(pid), signal.SIGTERM) + except ProcessLookupError: + logging.warning("Runbuilds not running or already killed") class TestCommands(TestCase): diff --git a/lib/toaster/tests/db/test_db.py b/lib/toaster/tests/db/test_db.py index 041042227..072ab9436 100644 --- a/lib/toaster/tests/db/test_db.py +++ b/lib/toaster/tests/db/test_db.py @@ -23,6 +23,7 @@ # SOFTWARE. import sys +import pytest try: from StringIO import StringIO @@ -47,7 +48,7 @@ def capture(command, *args, **kwargs): def makemigrations(): management.call_command('makemigrations') - +@pytest.mark.order(1) class MigrationTest(TestCase): def testPendingMigration(self): diff --git a/lib/toaster/tests/eventreplay/__init__.py b/lib/toaster/tests/eventreplay/__init__.py index 3606cba4f..8ed6792ef 100644 --- a/lib/toaster/tests/eventreplay/__init__.py +++ b/lib/toaster/tests/eventreplay/__init__.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # diff --git a/lib/toaster/tests/functional/functional_helpers.py b/lib/toaster/tests/functional/functional_helpers.py index 6a3f74baa..7c20437d1 100644 --- a/lib/toaster/tests/functional/functional_helpers.py +++ b/lib/toaster/tests/functional/functional_helpers.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster functional tests implementation # @@ -11,35 +11,55 @@ import os import logging import subprocess import signal -import time import re from tests.browser.selenium_helpers_base import SeleniumTestCaseBase -from tests.builds.buildtest import load_build_environment +from selenium.webdriver.common.by import By +from selenium.common.exceptions import NoSuchElementException logger = logging.getLogger("toaster") +toaster_processes = [] class SeleniumFunctionalTestCase(SeleniumTestCaseBase): - wait_toaster_time = 5 + wait_toaster_time = 10 @classmethod def setUpClass(cls): # So that the buildinfo helper uses the test database' if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ 'toastermain.settings_test': - raise RuntimeError("Please initialise django with the tests settings: " \ + raise RuntimeError("Please initialise django with the tests settings: " "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") - load_build_environment() + # Wait for any known toaster processes to exit + global toaster_processes + for toaster_process in toaster_processes: + try: + os.waitpid(toaster_process, os.WNOHANG) + except ChildProcessError: + pass # start toaster cmd = "bash -c 'source toaster start'" - p = subprocess.Popen( + start_process = subprocess.Popen( cmd, cwd=os.environ.get("BUILDDIR"), shell=True) - if p.wait() != 0: - raise RuntimeError("Can't initialize toaster") + toaster_processes = [start_process.pid] + if start_process.wait() != 0: + port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip() + message = '' + if port_use: + process_id = port_use.split()[1] + process = os.popen(f"ps -o cmd= -p {process_id}").read().strip() + message = f"Port 8000 occupied by {process}" + raise RuntimeError(f"Can't initialize toaster. {message}") + + builddir = os.environ.get("BUILDDIR") + with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: + toaster_processes.append(int(f.read())) + with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: + toaster_processes.append(int(f.read())) super(SeleniumFunctionalTestCase, cls).setUpClass() cls.live_server_url = 'http://localhost:8000/' @@ -48,22 +68,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): def tearDownClass(cls): super(SeleniumFunctionalTestCase, cls).tearDownClass() - # XXX: source toaster stop gets blocked, to review why? - # from now send SIGTERM by hand - time.sleep(cls.wait_toaster_time) - builddir = os.environ.get("BUILDDIR") + global toaster_processes - with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: - toastermain_pid = int(f.read()) - os.kill(toastermain_pid, signal.SIGTERM) - with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: - runbuilds_pid = int(f.read()) - os.kill(runbuilds_pid, signal.SIGTERM) + cmd = "bash -c 'source toaster stop'" + stop_process = subprocess.Popen( + cmd, + cwd=os.environ.get("BUILDDIR"), + shell=True) + # Toaster stop has been known to hang in these tests so force kill if it stalls + try: + if stop_process.wait(cls.wait_toaster_time) != 0: + raise Exception('Toaster stop process failed') + except Exception as e: + if e is subprocess.TimeoutExpired: + print('Toaster stop process took too long. Force killing toaster...') + else: + print('Toaster stop process failed. Force killing toaster...') + stop_process.kill() + for toaster_process in toaster_processes: + os.kill(toaster_process, signal.SIGTERM) def get_URL(self): rc=self.get_page_source() - project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) + project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc) return project_url.group(2) @@ -74,8 +102,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): """ try: table_element = self.get_table_element(table_id) - element = table_element.find_element_by_link_text(link_text) - except NoSuchElementException as e: + element = table_element.find_element(By.LINK_TEXT, link_text) + except NoSuchElementException: print('no element found') raise return element @@ -85,8 +113,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): #return whole-table element element_xpath = "//*[@id='" + table_id + "']" try: - element = self.driver.find_element_by_xpath(element_xpath) - except NoSuchElementException as e: + element = self.driver.find_element(By.XPATH, element_xpath) + except NoSuchElementException: raise return element row = coordinate[0] @@ -95,8 +123,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): #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 as e: + element = self.driver.find_element(By.XPATH, element_xpath) + except NoSuchElementException: return False return element #now we are looking for an element with specified X and Y @@ -104,7 +132,7 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" try: - element = self.driver.find_element_by_xpath(element_xpath) - except NoSuchElementException as e: + element = self.driver.find_element(By.XPATH, element_xpath) + except NoSuchElementException: return False return element diff --git a/lib/toaster/tests/functional/test_create_new_project.py b/lib/toaster/tests/functional/test_create_new_project.py new file mode 100644 index 000000000..cdfdd9ab2 --- /dev/null +++ b/lib/toaster/tests/functional/test_create_new_project.py @@ -0,0 +1,179 @@ +#! /usr/bin/env python3 +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import re +import pytest +from django.urls import reverse +from selenium.webdriver.support.select import Select +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from orm.models import Project +from selenium.webdriver.common.by import By + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestCreateNewProject(SeleniumFunctionalTestCase): + + def _create_test_new_project( + self, + project_name, + release, + release_title, + merge_toaster_settings, + ): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name', poll=3) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(release) + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if merge_toaster_settings: + if not checkbox.is_selected(): + checkbox.click() + else: + if checkbox.is_selected(): + checkbox.click() + + self.driver.find_element(By.ID, "create-project-button").click() + + element = self.wait_until_visible('#project-created-notification', poll=3) + self.assertTrue( + self.element_exists('#project-created-notification'), + f"Project:{project_name} creation notification not shown" + ) + self.assertTrue( + project_name in element.text, + f"New project name:{project_name} not in new project notification" + ) + self.assertTrue( + Project.objects.filter(name=project_name).count(), + f"New project:{project_name} not found in database" + ) + + # check release + self.assertTrue(re.search( + release_title, + self.driver.find_element(By.XPATH, + "//span[@id='project-release-title']" + ).text), + 'The project release is not defined') + + def test_create_new_project_master(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project master (option value: 3) + - Merge Toaster settings: False + """ + release = '3' + release_title = 'Yocto Project master' + project_name = 'projectmaster' + self._create_test_new_project( + project_name, + release, + release_title, + False, + ) + + def test_create_new_project_scarthgap(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project 5.0 "Scarthgap" (option value: 1) + - Merge Toaster settings: True + """ + release = '1' + release_title = 'Yocto Project 5.0 "Scarthgap"' + project_name = 'projectscarthgap' + self._create_test_new_project( + project_name, + release, + release_title, + True, + ) + + def test_create_new_project_kirkstone(self): + """ Test create new project using: + - Project Name: Any string + - Release: Yocto Project 4.0 "Kirkstone" (option value: 4) + - Merge Toaster settings: True + """ + release = '4' + release_title = 'Yocto Project 4.0 "Kirkstone"' + project_name = 'projectkirkstone' + self._create_test_new_project( + project_name, + release, + release_title, + True, + ) + + def test_create_new_project_local(self): + """ Test create new project using: + - Project Name: Any string + - Release: Local Yocto Project (option value: 2) + - Merge Toaster settings: True + """ + release = '2' + release_title = 'Local Yocto Project' + project_name = 'projectlocal' + self._create_test_new_project( + project_name, + release, + release_title, + True, + ) + + def test_create_new_project_without_name(self): + """ Test create new project without project name """ + self.get(reverse('newproject')) + + select = Select(self.find('#projectversion')) + select.select_by_value(str(3)) + + # Check input name has required attribute + input_name = self.driver.find_element(By.ID, "new-project-name") + self.assertIsNotNone(input_name.get_attribute('required'), + 'Input name has not required attribute') + + # Check create button is disabled + create_btn = self.driver.find_element(By.ID, "create-project-button") + self.assertIsNotNone(create_btn.get_attribute('disabled'), + 'Create button is not disabled') + + def test_import_new_project(self): + """ Test import new project using: + - Project Name: Any string + - Project type: select (Import command line project) + - Import existing project directory: Wrong Path + """ + project_name = 'projectimport' + self.get(reverse('newproject')) + self.driver.find_element(By.ID, + "new-project-name").send_keys(project_name) + # select import project + self.find('#type-import').click() + + # set wrong path + wrong_path = '/wrongpath' + self.driver.find_element(By.ID, + "import-project-dir").send_keys(wrong_path) + self.driver.find_element(By.ID, "create-project-button").click() + + # check error message + self.assertTrue(self.element_exists('.alert-danger'), + 'Allert message not shown') + self.assertTrue(wrong_path in self.find('.alert-danger').text, + "Wrong path not in alert message") diff --git a/lib/toaster/tests/functional/test_functional_basic.py b/lib/toaster/tests/functional/test_functional_basic.py index 2b3a2886c..e4070fbb8 100644 --- a/lib/toaster/tests/functional/test_functional_basic.py +++ b/lib/toaster/tests/functional/test_functional_basic.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster functional tests implementation # @@ -7,106 +7,130 @@ # SPDX-License-Identifier: GPL-2.0-only # -import time import re +from django.urls import reverse +import pytest from tests.functional.functional_helpers import SeleniumFunctionalTestCase from orm.models import Project +from selenium.webdriver.common.by import By +from tests.functional.utils import get_projectId_from_url + + +@pytest.mark.django_db +@pytest.mark.order("second_to_last") class FuntionalTestBasic(SeleniumFunctionalTestCase): + """Basic functional tests for Toaster""" + project_id = None + + def setUp(self): + super(FuntionalTestBasic, self).setUp() + if not FuntionalTestBasic.project_id: + self._create_slenium_project() + current_url = self.driver.current_url + FuntionalTestBasic.project_id = get_projectId_from_url(current_url) # testcase (1514) - def test_create_slenium_project(self): + def _create_slenium_project(self): project_name = 'selenium-project' - self.get('') - self.driver.find_element_by_link_text("To start building, create your first Toaster project").click() - self.driver.find_element_by_id("new-project-name").send_keys(project_name) - self.driver.find_element_by_id('projectversion').click() - self.driver.find_element_by_id("create-project-button").click() - element = self.wait_until_visible('#project-created-notification') + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name', poll=3) + self.driver.find_element(By.ID, "new-project-name").send_keys(project_name) + self.driver.find_element(By.ID, 'projectversion').click() + self.driver.find_element(By.ID, "create-project-button").click() + element = self.wait_until_visible('#project-created-notification', poll=10) self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') self.assertTrue(project_name in element.text, "New project name not in new project notification") self.assertTrue(Project.objects.filter(name=project_name).count(), "New project not found in database") + return Project.objects.last().id # testcase (1515) def test_verify_left_bar_menu(self): - self.get('') - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') project_URL=self.get_URL() - self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click() + self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click() + self.wait_until_present('#config-nav', poll=10) try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() - self.assertTrue(re.search("Custom images",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'Custom images information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Custom images",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'Custom images information is not loading properly') except: self.fail(msg='No Custom images tab available') try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() - self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly') except: self.fail(msg='No Compatible image tab available') try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() - self.assertTrue(re.search("Compatible software recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Compatible software recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly') except: self.fail(msg='No Compatible software recipe tab available') try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() - self.assertTrue(re.search("Compatible machines",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Compatible machines",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly') except: self.fail(msg='No Compatible machines tab available') try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() - self.assertTrue(re.search("Compatible layers",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Compatible layers",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly') except: self.fail(msg='No Compatible layers tab available') try: - self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() - self.assertTrue(re.search("Bitbake variables",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly') + self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Bitbake variables",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly') except: self.fail(msg='No Bitbake variables tab available') # testcase (1516) def test_review_configuration_information(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() project_URL=self.get_URL() - + self.wait_until_present('#config-nav', poll=10) try: self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') - self.assertTrue(re.search("qemux86",self.driver.find_element_by_xpath("//span[@id='project-machine-name']").text),'The machine type is not assigned') - self.driver.find_element_by_xpath("//span[@id='change-machine-toggle']").click() - self.wait_until_visible('#select-machine-form') - self.wait_until_visible('#cancel-machine-change') - self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() + self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned') + self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click() + self.wait_until_visible('#select-machine-form', poll=10) + self.wait_until_visible('#cancel-machine-change', poll=10) + self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() except: self.fail(msg='The machine information is wrong in the configuration page') try: - self.driver.find_element_by_id('no-most-built') + self.driver.find_element(By.ID, 'no-most-built') except: self.fail(msg='No Most built information in project detail page') try: - self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_xpath("//span[@id='project-release-title']").text),'The project release is not defined') + self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.XPATH, "//span[@id='project-release-title']").text),'The project release is not defined') except: self.fail(msg='No project release title information in project detail page') try: - self.driver.find_element_by_xpath("//div[@id='layer-container']") - self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') - layer_list = self.driver.find_element_by_id("layers-in-project-list") - layers = layer_list.find_elements_by_tag_name("li") + self.driver.find_element(By.XPATH, "//div[@id='layer-container']") + self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count') + layer_list = self.driver.find_element(By.ID, "layers-in-project-list") + layers = layer_list.find_elements(By.TAG_NAME, "li") for layer in layers: if re.match ("openembedded-core",layer.text): print ("openembedded-core layer is a default layer in the project configuration") @@ -121,61 +145,60 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): # testcase (1517) def test_verify_machine_information(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) try: self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') - self.assertTrue(re.search("qemux86",self.driver.find_element_by_id("project-machine-name").text),'The machine type is not assigned') - self.driver.find_element_by_id("change-machine-toggle").click() - self.wait_until_visible('#select-machine-form') - self.wait_until_visible('#cancel-machine-change') - self.driver.find_element_by_id("cancel-machine-change").click() + self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned') + self.driver.find_element(By.ID, "change-machine-toggle").click() + self.wait_until_visible('#select-machine-form', poll=10) + self.wait_until_visible('#cancel-machine-change', poll=10) + self.driver.find_element(By.ID, "cancel-machine-change").click() except: self.fail(msg='The machine information is wrong in the configuration page') # testcase (1518) def test_verify_most_built_recipes_information(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) project_URL=self.get_URL() - try: - self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element_by_id("no-most-built").text),'Default message of no builds is not present') - self.driver.find_element_by_xpath("//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() - self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly') + self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element(By.ID, "no-most-built").text),'Default message of no builds is not present') + self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly') except: self.fail(msg='No Most built information in project detail page') # testcase (1519) def test_verify_project_release_information(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) try: - self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined') + self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined') except: self.fail(msg='No project release title information in project detail page') # testcase (1520) def test_verify_layer_information(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) project_URL=self.get_URL() - try: - self.driver.find_element_by_xpath("//div[@id='layer-container']") - self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') - layer_list = self.driver.find_element_by_id("layers-in-project-list") - layers = layer_list.find_elements_by_tag_name("li") + self.driver.find_element(By.XPATH, "//div[@id='layer-container']") + self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count') + layer_list = self.driver.find_element(By.ID, "layers-in-project-list") + layers = layer_list.find_elements(By.TAG_NAME, "li") for layer in layers: if re.match ("openembedded-core",layer.text): @@ -187,43 +210,46 @@ class FuntionalTestBasic(SeleniumFunctionalTestCase): else: self.fail(msg='default layers are missing from the project configuration') - self.driver.find_element_by_xpath("//input[@id='layer-add-input']") - self.driver.find_element_by_xpath("//button[@id='add-layer-btn']") - self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") - self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") + self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']") + self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']") + self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") + self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") except: self.fail(msg='No Layer information in project detail page') # testcase (1521) def test_verify_project_detail_links(self): - self.get('') - self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() - self.wait_until_visible('#projectstable') + self.get(reverse('all-projects')) + self.wait_until_present('#projectstable', poll=10) self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.wait_until_present('#config-nav', poll=10) project_URL=self.get_URL() - - self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click() - self.assertTrue(re.search("Configuration",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled') + self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click() + self.wait_until_present('#config-nav', poll=10) + self.assertTrue(re.search("Configuration",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled') try: - self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() - self.assertTrue(re.search("Builds",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled') - self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']") + self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() + self.wait_until_visible('#project-topbar', poll=10) + self.assertTrue(re.search("Builds",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled') + self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']") except: self.fail(msg='Builds tab information is not present') try: - self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() - self.assertTrue(re.search("Import layer",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled') - self.driver.find_element_by_xpath("//fieldset[@id='repo-select']") - self.driver.find_element_by_xpath("//fieldset[@id='git-repo']") + self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() + self.wait_until_visible('#project-topbar', poll=10) + self.assertTrue(re.search("Import layer",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled') + self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']") + self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']") except: self.fail(msg='Import layer tab not loading properly') try: - self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() - self.assertTrue(re.search("New custom image",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled') - self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element_by_xpath("//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly') + self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() + self.wait_until_visible('#project-topbar', poll=10) + self.assertTrue(re.search("New custom image",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled') + self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element(By.XPATH, "//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly') except: self.fail(msg='New custom image tab not loading properly') diff --git a/lib/toaster/tests/functional/test_project_config.py b/lib/toaster/tests/functional/test_project_config.py new file mode 100644 index 000000000..dbee36aa4 --- /dev/null +++ b/lib/toaster/tests/functional/test_project_config.py @@ -0,0 +1,341 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import string +import random +import pytest +from django.urls import reverse +from selenium.webdriver import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import TimeoutException +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + +from .utils import get_projectId_from_url + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestProjectConfig(SeleniumFunctionalTestCase): + project_id = None + PROJECT_NAME = 'TestProjectConfig' + INVALID_PATH_START_TEXT = 'The directory path should either start with a /' + INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ + 'any of these characters' + + def _create_project(self, project_name): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name', poll=2) + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value('3') + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectConfig': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfig' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name', poll=2) + url = reverse('project', args=(TestProjectConfig.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def _navigate_bbv_page(self): + """ Navigate to project BitBake variables page """ + # check if the menu is displayed + if TestProjectConfig.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfig.project_id = get_projectId_from_url(current_url) + else: + url = reverse('projectconf', args=(TestProjectConfig.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + bbv_page_link = self._get_config_nav_item(9) + bbv_page_link.click() + self.wait_until_visible('#config-nav', poll=3) + + def test_no_underscore_iamgefs_type(self): + """ + Should not accept IMAGEFS_TYPE with an underscore + """ + self._navigate_bbv_page() + imagefs_type = "foo_bar" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + element = self.wait_until_visible('#hintError-image-fs_type', poll=2) + + self.assertTrue(("A valid image type cannot include underscores" in element.text), + "Did not find underscore error message") + + def test_checkbox_verification(self): + """ + Should automatically check the checkbox if user enters value + text box, if value is there in the checkbox. + """ + self._navigate_bbv_page() + + imagefs_type = "btrfs" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "btrfs": + self.assertEqual(checkbox.is_selected(), True) + + def test_textbox_with_checkbox_verification(self): + """ + Should automatically add or remove value in textbox, if user checks + or unchecks checkboxes. + """ + self._navigate_bbv_page() + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + checkboxes_selector = '.fs-checkbox-fstypes' + + self.wait_until_visible(checkboxes_selector, poll=2) + checkboxes = self.find_all(checkboxes_selector) + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "cpio": + checkbox.click() + element = self.driver.find_element(By.ID, 'new-imagefs_types') + + self.wait_until_visible('#new-imagefs_types', poll=2) + + self.assertTrue(("cpio" in element.get_attribute('value'), + "Imagefs not added into the textbox")) + checkbox.click() + self.assertTrue(("cpio" not in element.text), + "Image still present in the textbox") + + def test_set_download_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + DL_DIR + """ + self._navigate_bbv_page() + + # activate the input to edit download dir + try: + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + except TimeoutException: + # If download dir is not displayed, test is skipped + change_dl_dir_btn = None + + if change_dl_dir_btn: + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + change_dl_dir_btn.click() + + # downloads dir path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-dl_dir', poll=2) + input_field.clear() + self.enter_text('#new-dl_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2) + + msg = 'downloads directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # downloads dir path has a space + self.driver.find_element(By.ID, 'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with / + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + # downloads dir path starts with ${...} + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + def test_set_sstate_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + SSTATE_DIR + """ + self._navigate_bbv_page() + + try: + btn_chg_sstate_dir = self.wait_until_visible( + '#change-sstate_dir-icon', + poll=2 + ) + self.click('#change-sstate_dir-icon') + except TimeoutException: + # If sstate_dir is not displayed, test is skipped + btn_chg_sstate_dir = None + + if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed + # path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-sstate_dir', poll=2) + input_field.clear() + self.enter_text('#new-sstate_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2) + + msg = 'sstate directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # path has a space + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with / + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + # paths starts with ${...} + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + def _change_bbv_value(self, **kwargs): + var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values() + """ Change bitbake variable value """ + self._navigate_bbv_page() + self.wait_until_visible(f'#{btn_id}', poll=2) + if kwargs.get('new_variable'): + self.find(f"#{btn_id}").clear() + self.enter_text(f"#{btn_id}", f"{var_name}") + else: + self.click(f'#{btn_id}') + self.wait_until_visible(f'#{input_id}', poll=2) + + if kwargs.get('is_select'): + select = Select(self.find(f'#{input_id}')) + select.select_by_visible_text(value) + else: + self.find(f"#{input_id}").clear() + self.enter_text(f'#{input_id}', f'{value}') + self.click(f'#{save_btn}') + value_displayed = str(self.wait_until_visible(f'#{field}').text).lower() + msg = f'{var_name} variable not changed' + self.assertTrue(str(value).lower() in value_displayed, msg) + + def test_change_distro_var(self): + """ Test changing distro variable """ + self._change_bbv_value( + var_name='DISTRO', + field='distro', + btn_id='change-distro-icon', + input_id='new-distro', + value='poky-changed', + save_btn="apply-change-distro", + ) + + def test_set_image_install_append_var(self): + """ Test setting IMAGE_INSTALL:append variable """ + self._change_bbv_value( + var_name='IMAGE_INSTALL:append', + field='image_install', + btn_id='change-image_install-icon', + input_id='new-image_install', + value='bash, apt, busybox', + save_btn="apply-change-image_install", + ) + + def test_set_package_classes_var(self): + """ Test setting PACKAGE_CLASSES variable """ + self._change_bbv_value( + var_name='PACKAGE_CLASSES', + field='package_classes', + btn_id='change-package_classes-icon', + input_id='package_classes-select', + value='package_deb', + save_btn="apply-change-package_classes", + is_select=True, + ) + + def test_create_new_bbv(self): + """ Test creating new bitbake variable """ + self._change_bbv_value( + var_name='New_Custom_Variable', + field='configvar-list', + btn_id='variable', + input_id='value', + value='new variable value', + save_btn="add-configvar-button", + new_variable=True + ) diff --git a/lib/toaster/tests/functional/test_project_page.py b/lib/toaster/tests/functional/test_project_page.py new file mode 100644 index 000000000..0e36b44ff --- /dev/null +++ b/lib/toaster/tests/functional/test_project_page.py @@ -0,0 +1,792 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import os +import random +import string +from unittest import skip +import pytest +from django.urls import reverse +from django.utils import timezone +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import TimeoutException +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from orm.models import Build, Project, Target +from selenium.webdriver.common.by import By + +from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestProjectPage(SeleniumFunctionalTestCase): + project_id = None + PROJECT_NAME = 'TestProjectPage' + + def _create_project(self, project_name): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name') + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value('3') + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectPage': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectPage' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name') + url = reverse('project', args=(TestProjectPage.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _navigate_to_project_page(self): + # Navigate to project page + if TestProjectPage.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectPage.project_id = get_projectId_from_url(current_url) + else: + url = reverse('project', args=(TestProjectPage.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav') + + def _get_create_builds(self, **kwargs): + """ Create a build and return the build object """ + # parameters for builds to associate with the projects + now = timezone.now() + self.project1_build_success = { + 'project': Project.objects.get(id=TestProjectPage.project_id), + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + self.project1_build_failure = { + 'project': Project.objects.get(id=TestProjectPage.project_id), + 'started_on': now, + 'completed_on': now, + 'outcome': Build.FAILED + } + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + if kwargs: + # Create kwargs.get('success') builds with success status with target + # and kwargs.get('failure') builds with failure status with target + for i in range(kwargs.get('success', 0)): + now = timezone.now() + self.project1_build_success['started_on'] = now + self.project1_build_success[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, + target=f'{i}_success_recipe', + task=f'{i}_success_task') + + for i in range(kwargs.get('failure', 0)): + now = timezone.now() + self.project1_build_failure['started_on'] = now + self.project1_build_failure[ + 'completed_on'] = now - timezone.timedelta(days=i) + build = Build.objects.create(**self.project1_build_failure) + Target.objects.create(build=build, + target=f'{i}_fail_recipe', + task=f'{i}_fail_task') + return build1, build2 + + def _mixin_test_table_edit_column( + self, + table_id, + edit_btn_id, + list_check_box_id: list + ): + # Check edit column + edit_column = self.find(f'#{edit_btn_id}') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + for check_box_id in list_check_box_id: + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#{table_id} thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#{table_id} thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#{table_id} thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#{table_id} thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + + def _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def _navigate_to_config_nav(self, nav_id, nav_index): + # navigate to the project page + self._navigate_to_project_page() + # click on "Software recipe" tab + soft_recipe = self._get_config_nav_item(nav_index) + soft_recipe.click() + self.wait_until_visible(f'#{nav_id}') + + def _mixin_test_table_show_rows(self, table_selector, **kwargs): + """ Test the show rows feature in the builds table on the all builds page """ + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible(f'#{table_selector} tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all(f'#{table_selector} tbody tr')) > 0 + ) + self.wait_until_present(f'#{table_selector} tbody tr') + show_rows = self.driver.find_elements( + By.XPATH, + f'//select[@class="form-control pagesize-{table_selector}"]' + ) + rows_to_show = [10, 25, 50, 100, 150] + to_skip = kwargs.get('to_skip', []) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + for row_to_show in rows_to_show: + if row_to_show not in to_skip: + test_show_rows(row_to_show, show_row_link) + + def _mixin_test_table_search_input(self, **kwargs): + input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() + # Test search input + self.wait_until_visible(f'#{input_selector}') + recipe_input = self.find(f'#{input_selector}') + recipe_input.send_keys(input_text) + self.find(f'#{searchBtn_selector}').click() + self.wait_until_visible(f'#{table_selector} tbody tr') + rows = self.find_all(f'#{table_selector} tbody tr') + self.assertTrue(len(rows) > 0) + + def test_create_project(self): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self._create_project(project_name=self.PROJECT_NAME) + + def test_image_recipe_editColumn(self): + """ Test the edit column feature in image recipe table on project page """ + self._get_create_builds(success=10, failure=10) + + url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,)) + self.get(url) + self.wait_until_present('#imagerecipestable tbody tr') + + column_list = [ + 'get_description_or_summary', 'layer_version__get_vcs_reference', + 'layer_version__layer__name', 'license', 'recipe-file', 'section', + 'version' + ] + + # Check that we can hide the edit column + self._mixin_test_table_edit_column( + 'imagerecipestable', + 'edit-columns-button', + [f'checkbox-{column}' for column in column_list] + ) + + def test_page_header_on_project_page(self): + """ Check page header in project page: + - AT LEFT -> Logo of Yocto project, displayed, clickable + - "Toaster"+" Information icon", displayed, clickable + - "Server Icon" + "All builds", displayed, clickable + - "Directory Icon" + "All projects", displayed, clickable + - "Book Icon" + "Documentation", displayed, clickable + - AT RIGHT -> button "New project", displayed, clickable + """ + # navigate to the project page + self._navigate_to_project_page() + + # check page header + # AT LEFT -> Logo of Yocto project + logo = self.driver.find_element( + By.XPATH, + "//div[@class='toaster-navbar-brand']", + ) + logo_img = logo.find_element(By.TAG_NAME, 'img') + self.assertTrue(logo_img.is_displayed(), + 'Logo of Yocto project not found') + self.assertTrue( + '/static/img/logo.png' in str(logo_img.get_attribute('src')), + 'Logo of Yocto project not found' + ) + # "Toaster"+" Information icon", clickable + toaster = self.driver.find_element( + By.XPATH, + "//div[@class='toaster-navbar-brand']//a[@class='brand']", + ) + self.assertTrue(toaster.is_displayed(), 'Toaster not found') + self.assertTrue(toaster.text == 'Toaster') + info_sign = self.find('.glyphicon-info-sign') + self.assertTrue(info_sign.is_displayed()) + + # "Server Icon" + "All builds" + all_builds = self.find('#navbar-all-builds') + all_builds_link = all_builds.find_element(By.TAG_NAME, 'a') + self.assertTrue("All builds" in all_builds_link.text) + self.assertTrue( + '/toastergui/builds/' in str(all_builds_link.get_attribute('href')) + ) + server_icon = all_builds.find_element(By.TAG_NAME, 'i') + self.assertTrue( + server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks' + ) + self.assertTrue(server_icon.is_displayed()) + + # "Directory Icon" + "All projects" + all_projects = self.find('#navbar-all-projects') + all_projects_link = all_projects.find_element(By.TAG_NAME, 'a') + self.assertTrue("All projects" in all_projects_link.text) + self.assertTrue( + '/toastergui/projects/' in str(all_projects_link.get_attribute( + 'href')) + ) + dir_icon = all_projects.find_element(By.TAG_NAME, 'i') + self.assertTrue( + dir_icon.get_attribute('class') == 'icon-folder-open' + ) + self.assertTrue(dir_icon.is_displayed()) + + # "Book Icon" + "Documentation" + toaster_docs_link = self.find('#navbar-docs') + toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME, + 'a') + self.assertTrue("Documentation" in toaster_docs_link_link.text) + self.assertTrue( + toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual' + ) + book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i') + self.assertTrue( + book_icon.get_attribute('class') == 'glyphicon glyphicon-book' + ) + self.assertTrue(book_icon.is_displayed()) + + # AT RIGHT -> button "New project" + new_project_button = self.find('#new-project-button') + self.assertTrue(new_project_button.is_displayed()) + self.assertTrue(new_project_button.text == 'New project') + new_project_button.click() + self.assertTrue( + '/toastergui/newproject/' in str(self.driver.current_url) + ) + + def test_edit_project_name(self): + """ Test edit project name: + - Click on "Edit" icon button + - Change project name + - Click on "Save" button + - Check project name is changed + """ + # navigate to the project page + self._navigate_to_project_page() + + # click on "Edit" icon button + self.wait_until_visible('#project-name-container') + edit_button = self.find('#project-change-form-toggle') + edit_button.click() + project_name_input = self.find('#project-name-change-input') + self.assertTrue(project_name_input.is_displayed()) + project_name_input.clear() + project_name_input.send_keys('New Name') + self.find('#project-name-change-btn').click() + + # check project name is changed + self.wait_until_visible('#project-name-container') + self.assertTrue( + 'New Name' in str(self.find('#project-name-container').text) + ) + + def test_project_page_tabs(self): + """ Test project tabs: + - "configuration" tab + - "Builds" tab + - "Import layers" tab + - "New custom image" tab + Check search box used to build recipes + """ + # navigate to the project page + self._navigate_to_project_page() + + # check "configuration" tab + self.wait_until_visible('#topbar-configuration-tab') + config_tab = self.find('#topbar-configuration-tab') + self.assertTrue(config_tab.get_attribute('class') == 'active') + self.assertTrue('Configuration' in str(config_tab.text)) + self.assertTrue( + f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url) + ) + + def get_tabs(): + # tabs links list + return self.driver.find_elements( + By.XPATH, + '//div[@id="project-topbar"]//li' + ) + + def check_tab_link(tab_index, tab_name, url): + tab = get_tabs()[tab_index] + tab_link = tab.find_element(By.TAG_NAME, 'a') + self.assertTrue(url in tab_link.get_attribute('href')) + self.assertTrue(tab_name in tab_link.text) + self.assertTrue(tab.get_attribute('class') == 'active') + + # check "Builds" tab + builds_tab = get_tabs()[1] + builds_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 1, + 'Builds', + f"/toastergui/project/{TestProjectPage.project_id}/builds" + ) + + # check "Import layers" tab + import_layers_tab = get_tabs()[2] + import_layers_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 2, + 'Import layer', + f"/toastergui/project/{TestProjectPage.project_id}/importlayer" + ) + + # check "New custom image" tab + new_custom_image_tab = get_tabs()[3] + new_custom_image_tab.find_element(By.TAG_NAME, 'a').click() + check_tab_link( + 3, + 'New custom image', + f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage" + ) + + # check search box can be use to build recipes + search_box = self.find('#build-input') + search_box.send_keys('core-image-minimal') + self.find('#build-button').click() + self.wait_until_visible('#latest-builds') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]', + ) + last_build = lastest_builds[0] + self.assertTrue( + 'core-image-minimal' in str(last_build.text) + ) + + def test_softwareRecipe_page(self): + """ Test software recipe page + - Check title "Compatible software recipes" is displayed + - Check search input + - Check "build recipe" button works + - Check software recipe table feature(show/hide column, pagination) + """ + self._navigate_to_config_nav('softwarerecipestable', 4) + # check title "Compatible software recipes" is displayed + self.assertTrue("Compatible software recipes" in self.get_page_source()) + # Test search input + self._mixin_test_table_search_input( + input_selector='search-input-softwarerecipestable', + input_text='busybox', + searchBtn_selector='search-submit-softwarerecipestable', + table_selector='softwarerecipestable' + ) + # check "build recipe" button works + rows = self.find_all('#softwarerecipestable tbody tr') + image_to_build = rows[0] + build_btn = image_to_build.find_element( + By.XPATH, + '//td[@class="add-del-layers"]//a[1]' + ) + build_btn.click() + build_state = wait_until_build(self, 'queued cloning starting parsing failed') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + self.assertTrue(len(lastest_builds) > 0) + last_build = lastest_builds[0] + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) + + # check software recipe table feature(show/hide column, pagination) + self._navigate_to_config_nav('softwarerecipestable', 4) + column_list = [ + 'get_description_or_summary', + 'layer_version__get_vcs_reference', + 'layer_version__layer__name', + 'license', + 'recipe-file', + 'section', + 'version', + ] + self._mixin_test_table_edit_column( + 'softwarerecipestable', + 'edit-columns-button', + [f'checkbox-{column}' for column in column_list] + ) + self._navigate_to_config_nav('softwarerecipestable', 4) + # check show rows(pagination) + self._mixin_test_table_show_rows( + table_selector='softwarerecipestable', + to_skip=[150], + ) + + def test_machines_page(self): + """ Test Machine page + - Check if title "Compatible machines" is displayed + - Check search input + - Check "Select machine" button works + - Check "Add layer" button works + - Check Machine table feature(show/hide column, pagination) + """ + self._navigate_to_config_nav('machinestable', 5) + # check title "Compatible software recipes" is displayed + self.assertTrue("Compatible machines" in self.get_page_source()) + # Test search input + self._mixin_test_table_search_input( + input_selector='search-input-machinestable', + input_text='qemux86-64', + searchBtn_selector='search-submit-machinestable', + table_selector='machinestable' + ) + # check "Select machine" button works + rows = self.find_all('#machinestable tbody tr') + machine_to_select = rows[0] + select_btn = machine_to_select.find_element( + By.XPATH, + '//td[@class="add-del-layers"]//a[1]' + ) + select_btn.send_keys(Keys.RETURN) + self.wait_until_visible('#config-nav') + project_machine_name = self.find('#project-machine-name') + self.assertTrue( + 'qemux86-64' in project_machine_name.text + ) + # check "Add layer" button works + self._navigate_to_config_nav('machinestable', 5) + # Search for a machine whit layer not in project + self._mixin_test_table_search_input( + input_selector='search-input-machinestable', + input_text='qemux86-64-tpm2', + searchBtn_selector='search-submit-machinestable', + table_selector='machinestable' + ) + self.wait_until_visible('#machinestable tbody tr', poll=3) + rows = self.find_all('#machinestable tbody tr') + machine_to_add = rows[0] + add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]') + add_btn.click() + self.wait_until_visible('#change-notification') + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have added 1 layer to your project' in str(change_notification.text) + ) + # check Machine table feature(show/hide column, pagination) + self._navigate_to_config_nav('machinestable', 5) + column_list = [ + 'description', + 'layer_version__get_vcs_reference', + 'layer_version__layer__name', + 'machinefile', + ] + self._mixin_test_table_edit_column( + 'machinestable', + 'edit-columns-button', + [f'checkbox-{column}' for column in column_list] + ) + self._navigate_to_config_nav('machinestable', 5) + # check show rows(pagination) + self._mixin_test_table_show_rows( + table_selector='machinestable', + to_skip=[150], + ) + + def test_layers_page(self): + """ Test layers page + - Check if title "Compatible layerss" is displayed + - Check search input + - Check "Add layer" button works + - Check "Remove layer" button works + - Check layers table feature(show/hide column, pagination) + """ + self._navigate_to_config_nav('layerstable', 6) + # check title "Compatible layers" is displayed + self.assertTrue("Compatible layers" in self.get_page_source()) + # Test search input + input_text='meta-tanowrt' + self._mixin_test_table_search_input( + input_selector='search-input-layerstable', + input_text=input_text, + searchBtn_selector='search-submit-layerstable', + table_selector='layerstable' + ) + # check "Add layer" button works + self.wait_until_visible('#layerstable tbody tr', poll=3) + rows = self.find_all('#layerstable tbody tr') + layer_to_add = rows[0] + add_btn = layer_to_add.find_element( + By.XPATH, + '//td[@class="add-del-layers"]' + ) + add_btn.click() + # check modal is displayed + self.wait_until_visible('#dependencies-modal', poll=3) + list_dependencies = self.find_all('#dependencies-list li') + # click on add-layers button + add_layers_btn = self.driver.find_element( + By.XPATH, + '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]' + ) + add_layers_btn.click() + self.wait_until_visible('#change-notification') + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text) + ) + # check "Remove layer" button works + self.wait_until_visible('#layerstable tbody tr', poll=3) + rows = self.find_all('#layerstable tbody tr') + layer_to_remove = rows[0] + remove_btn = layer_to_remove.find_element( + By.XPATH, + '//td[@class="add-del-layers"]' + ) + remove_btn.click() + self.wait_until_visible('#change-notification', poll=2) + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text) + ) + # check layers table feature(show/hide column, pagination) + self._navigate_to_config_nav('layerstable', 6) + column_list = [ + 'dependencies', + 'revision', + 'layer__vcs_url', + 'git_subdir', + 'layer__summary', + ] + self._mixin_test_table_edit_column( + 'layerstable', + 'edit-columns-button', + [f'checkbox-{column}' for column in column_list] + ) + self._navigate_to_config_nav('layerstable', 6) + # check show rows(pagination) + self._mixin_test_table_show_rows( + table_selector='layerstable', + to_skip=[150], + ) + + def test_distro_page(self): + """ Test distros page + - Check if title "Compatible distros" is displayed + - Check search input + - Check "Add layer" button works + - Check distro table feature(show/hide column, pagination) + """ + self._navigate_to_config_nav('distrostable', 7) + # check title "Compatible distros" is displayed + self.assertTrue("Compatible Distros" in self.get_page_source()) + # Test search input + input_text='poky-altcfg' + self._mixin_test_table_search_input( + input_selector='search-input-distrostable', + input_text=input_text, + searchBtn_selector='search-submit-distrostable', + table_selector='distrostable' + ) + # check "Add distro" button works + rows = self.find_all('#distrostable tbody tr') + distro_to_add = rows[0] + add_btn = distro_to_add.find_element( + By.XPATH, + '//td[@class="add-del-layers"]//a[1]' + ) + add_btn.click() + self.wait_until_visible('#change-notification', poll=2) + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have changed the distro to: {input_text}' in str(change_notification.text) + ) + # check distro table feature(show/hide column, pagination) + self._navigate_to_config_nav('distrostable', 7) + column_list = [ + 'description', + 'templatefile', + 'layer_version__get_vcs_reference', + 'layer_version__layer__name', + ] + self._mixin_test_table_edit_column( + 'distrostable', + 'edit-columns-button', + [f'checkbox-{column}' for column in column_list] + ) + self._navigate_to_config_nav('distrostable', 7) + # check show rows(pagination) + self._mixin_test_table_show_rows( + table_selector='distrostable', + to_skip=[150], + ) + + def test_single_layer_page(self): + """ Test layer page + - Check if title is displayed + - Check add/remove layer button works + - Check tabs(layers, recipes, machines) are displayed + - Check left section is displayed + - Check layer name + - Check layer summary + - Check layer description + """ + url = reverse("layerdetails", args=(TestProjectPage.project_id, 7)) + self.get(url) + self.wait_until_visible('.page-header') + # check title is displayed + self.assertTrue(self.find('.page-header h1').is_displayed()) + + # check add layer button works + remove_layer_btn = self.find('#add-remove-layer-btn') + remove_layer_btn.click() + self.wait_until_visible('#change-notification', poll=2) + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have removed 1 layer from your project' in str(change_notification.text) + ) + # check add layer button works, 18 is the random layer id + add_layer_btn = self.find('#add-remove-layer-btn') + add_layer_btn.click() + self.wait_until_visible('#change-notification') + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have added 1 layer to your project' in str(change_notification.text) + ) + # check tabs(layers, recipes, machines) are displayed + tabs = self.find_all('.nav-tabs li') + self.assertEqual(len(tabs), 3) + # Check first tab + tabs[0].click() + self.assertTrue( + 'active' in str(self.find('#information').get_attribute('class')) + ) + # Check second tab + tabs[1].click() + self.assertTrue( + 'active' in str(self.find('#recipes').get_attribute('class')) + ) + # Check third tab + tabs[2].click() + self.assertTrue( + 'active' in str(self.find('#machines').get_attribute('class')) + ) + # Check left section is displayed + section = self.find('.well') + # Check layer name + self.assertTrue( + section.find_element(By.XPATH, '//h2[1]').is_displayed() + ) + # Check layer summary + self.assertTrue("Summary" in section.text) + # Check layer description + self.assertTrue("Description" in section.text) + + def test_single_recipe_page(self): + """ Test recipe page + - Check if title is displayed + - Check add recipe layer displayed + - Check left section is displayed + - Check recipe: name, summary, description, Version, Section, + License, Approx. packages included, Approx. size, Recipe file + """ + url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428)) + self.get(url) + self.wait_until_visible('.page-header') + # check title is displayed + self.assertTrue(self.find('.page-header h1').is_displayed()) + # check add recipe layer displayed + add_recipe_layer_btn = self.find('#add-layer-btn') + self.assertTrue(add_recipe_layer_btn.is_displayed()) + # check left section is displayed + section = self.find('.well') + # Check recipe name + self.assertTrue( + section.find_element(By.XPATH, '//h2[1]').is_displayed() + ) + # Check recipe sections details info are displayed + self.assertTrue("Summary" in section.text) + self.assertTrue("Description" in section.text) + self.assertTrue("Version" in section.text) + self.assertTrue("Section" in section.text) + self.assertTrue("License" in section.text) + self.assertTrue("Approx. packages included" in section.text) + self.assertTrue("Approx. package size" in section.text) + self.assertTrue("Recipe file" in section.text) diff --git a/lib/toaster/tests/functional/test_project_page_tab_config.py b/lib/toaster/tests/functional/test_project_page_tab_config.py new file mode 100644 index 000000000..eb905ddf3 --- /dev/null +++ b/lib/toaster/tests/functional/test_project_page_tab_config.py @@ -0,0 +1,528 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import string +import random +import pytest +from django.urls import reverse +from selenium.webdriver import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException +from orm.models import Project +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + +from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestProjectConfigTab(SeleniumFunctionalTestCase): + PROJECT_NAME = 'TestProjectConfigTab' + project_id = None + + def _create_project(self, project_name, **kwargs): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + release = kwargs.get('release', '3') + self.get(reverse('newproject')) + self.wait_until_visible('#new-project-name') + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value(release) + + # check merge toaster settings + checkbox = self.find('.checkbox-mergeattr') + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectConfigTab': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfigTab' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name', poll=3) + url = reverse('project', args=(TestProjectConfigTab.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _navigate_to_project_page(self): + # Navigate to project page + if TestProjectConfigTab.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url( + current_url) + else: + url = reverse('project', args=(TestProjectConfigTab.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav') + + def _create_builds(self): + # check search box can be use to build recipes + search_box = self.find('#build-input') + search_box.send_keys('foo') + self.find('#build-button').click() + self.wait_until_present('#latest-builds') + # loop until reach the parsing state + wait_until_build(self, 'queued cloning starting parsing failed') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div', + ) + last_build = lastest_builds[0] + self.assertTrue( + 'foo' in str(last_build.text) + ) + last_build = lastest_builds[0] + try: + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + except NoSuchElementException: + # Skip if the build is already cancelled + pass + wait_until_build_cancelled(self) + + def _get_tabs(self): + # tabs links list + return self.driver.find_elements( + By.XPATH, + '//div[@id="project-topbar"]//li' + ) + + def _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def test_project_config_nav(self): + """ Test project config tab navigation: + - Check if the menu is displayed and contains the right elements: + - Configuration + - COMPATIBLE METADATA + - Custom images + - Image recipes + - Software recipes + - Machines + - Layers + - Distro + - EXTRA CONFIGURATION + - Bitbake variables + - Actions + - Delete project + """ + self._navigate_to_project_page() + + def _get_config_nav_item(index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def check_config_nav_item(index, item_name, url): + item = _get_config_nav_item(index) + self.assertTrue(item_name in item.text) + self.assertTrue(item.get_attribute('class') == 'active') + self.assertTrue(url in self.driver.current_url) + + # check if the menu contains the right elements + # COMPATIBLE METADATA + compatible_metadata = _get_config_nav_item(1) + self.assertTrue( + "compatible metadata" in compatible_metadata.text.lower() + ) + # EXTRA CONFIGURATION + extra_configuration = _get_config_nav_item(8) + self.assertTrue( + "extra configuration" in extra_configuration.text.lower() + ) + # Actions + actions = _get_config_nav_item(10) + self.assertTrue("actions" in str(actions.text).lower()) + + conf_nav_list = [ + # config + [0, 'Configuration', + f"/toastergui/project/{TestProjectConfigTab.project_id}"], + # custom images + [2, 'Custom images', + f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"], + # image recipes + [3, 'Image recipes', + f"/toastergui/project/{TestProjectConfigTab.project_id}/images"], + # software recipes + [4, 'Software recipes', + f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"], + # machines + [5, 'Machines', + f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"], + # layers + [6, 'Layers', + f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"], + # distro + [7, 'Distros', + f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"], + # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables + ] + for index, item_name, url in conf_nav_list: + item = _get_config_nav_item(index) + if item.get_attribute('class') != 'active': + item.click() + check_config_nav_item(index, item_name, url) + + def test_image_recipe_editColumn(self): + """ Test the edit column feature in image recipe table on project page """ + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + self.wait_until_present('#imagerecipestable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-get_description_or_summary') + test_edit_column('checkbox-layer_version__get_vcs_reference') + test_edit_column('checkbox-layer_version__layer__name') + test_edit_column('checkbox-license') + test_edit_column('checkbox-recipe-file') + test_edit_column('checkbox-section') + test_edit_column('checkbox-version') + + def test_image_recipe_show_rows(self): + """ Test the show rows feature in image recipe table on project page """ + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#imagerecipestable tbody tr', poll=3) + # check at least some rows are visible + self.assertTrue( + len(self.find_all('#imagerecipestable tbody tr')) > 0 + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + self.wait_until_present('#imagerecipestable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-imagerecipestable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) + + def test_project_config_tab_right_section(self): + """ Test project config tab right section contains five blocks: + - Machine: + - check 'Machine' is displayed + - check can change Machine + - Distro: + - check 'Distro' is displayed + - check can change Distro + - Most built recipes: + - check 'Most built recipes' is displayed + - check can select a recipe and build it + - Project release: + - check 'Project release' is displayed + - check project has right release displayed + - Layers: + - check can add a layer if exists + - check at least three layers are displayed + - openembedded-core + - meta-poky + - meta-yocto-bsp + """ + # Create a new project for this test + project_name = self._random_string(10) + self._create_project(project_name=project_name) + # check if the menu is displayed + self.wait_until_visible('#project-page') + block_l = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[2]') + project_release = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[1]/div[4]') + layers = block_l.find_element(By.ID, 'layer-container') + + def check_machine_distro(self, item_name, new_item_name, block_id): + block = self.find(f'#{block_id}') + title = block.find_element(By.TAG_NAME, 'h3') + self.assertTrue(item_name.capitalize() in title.text) + edit_btn = self.find(f'#change-{item_name}-toggle') + edit_btn.click() + self.wait_until_visible(f'#{item_name}-change-input') + name_input = self.find(f'#{item_name}-change-input') + name_input.clear() + name_input.send_keys(new_item_name) + change_btn = self.find(f'#{item_name}-change-btn') + change_btn.click() + self.wait_until_visible(f'#project-{item_name}-name') + project_name = self.find(f'#project-{item_name}-name') + self.assertTrue(new_item_name in project_name.text) + # check change notificaiton is displayed + change_notification = self.find('#change-notification') + self.assertTrue( + f'You have changed the {item_name} to: {new_item_name}' in change_notification.text + ) + + # Machine + check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section') + # Distro + check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section') + + # Project release + title = project_release.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Project release" in title.text) + self.assertTrue( + "Yocto Project master" in self.find('#project-release-title').text + ) + # Layers + title = layers.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Layers" in title.text) + # check at least three layers are displayed + # openembedded-core + # meta-poky + # meta-yocto-bsp + layers_list = layers.find_element(By.ID, 'layers-in-project-list') + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + # remove all layers except the first three layers + for i in range(3, len(layers_list_items)): + layers_list_items[i].find_element(By.TAG_NAME, 'span').click() + # check can add a layer if exists + add_layer_input = layers.find_element(By.ID, 'layer-add-input') + add_layer_input.send_keys('meta-oe') + self.wait_until_visible('#layer-container > form > div > span > div') + dropdown_item = self.driver.find_element( + By.XPATH, + '//*[@id="layer-container"]/form/div/span/div' + ) + try: + dropdown_item.click() + except ElementClickInterceptedException: + self.skipTest( + "layer-container dropdown item click intercepted. Element not properly visible.") + add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') + add_layer_btn.click() + self.wait_until_visible('#layers-in-project-list') + # check layer is added + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue(len(layers_list_items) == 4) + + def test_most_build_recipes(self): + """ Test most build recipes block contains""" + def rebuild_from_most_build_recipes(recipe_list_items): + checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') + checkbox.click() + build_btn = self.find('#freq-build-btn') + build_btn.click() + self.wait_until_visible('#latest-builds') + wait_until_build(self, 'queued cloning starting parsing failed') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + self.assertTrue(len(lastest_builds) >= 2) + last_build = lastest_builds[0] + try: + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + except NoSuchElementException: + # Skip if the build is already cancelled + pass + wait_until_build_cancelled(self) + # Create a new project for remaining asserts + project_name = self._random_string(10) + self._create_project(project_name=project_name, release='2') + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) + url = current_url.split('?')[0] + + # Create a new builds + self._create_builds() + + # back to project page + self.driver.get(url) + + self.wait_until_visible('#project-page', poll=3) + + # Most built recipes + most_built_recipes = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') + title = most_built_recipes.find_element(By.TAG_NAME, 'h3') + self.assertTrue("Most built recipes" in title.text) + # check can select a recipe and build it + self.wait_until_visible('#freq-build-list', poll=3) + recipe_list = self.find('#freq-build-list') + recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue( + len(recipe_list_items) > 0, + msg="Any recipes found in the most built recipes list", + ) + rebuild_from_most_build_recipes(recipe_list_items) + TestProjectConfigTab.project_id = None # reset project id + + def test_project_page_tab_importlayer(self): + """ Test project page tab import layer """ + self._navigate_to_project_page() + # navigate to "Import layers" tab + import_layers_tab = self._get_tabs()[2] + import_layers_tab.find_element(By.TAG_NAME, 'a').click() + self.wait_until_visible('#layer-git-repo-url') + + # Check git repo radio button + git_repo_radio = self.find('#git-repo-radio') + git_repo_radio.click() + + # Set git repo url + input_repo_url = self.find('#layer-git-repo-url') + input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake') + # Blur the input to trigger the validation + input_repo_url.send_keys(Keys.TAB) + + # Check name is set + input_layer_name = self.find('#import-layer-name') + self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake') + + # Set branch + input_branch = self.find('#layer-git-ref') + input_branch.send_keys('master') + + # Import layer + self.find('#import-and-add-btn').click() + + # Check layer is added + self.wait_until_visible('#layer-container') + block_l = self.driver.find_element( + By.XPATH, '//*[@id="project-page"]/div[2]') + layers = block_l.find_element(By.ID, 'layer-container') + layers_list = layers.find_element(By.ID, 'layers-in-project-list') + layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') + self.assertTrue( + 'meta-fake' in str(layers_list_items[-1].text) + ) + + def test_project_page_custom_image_no_image(self): + """ Test project page tab "New custom image" when no custom image """ + project_name = self._random_string(10) + self._create_project(project_name=project_name) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) + # navigate to "Custom image" tab + custom_image_section = self._get_config_nav_item(2) + custom_image_section.click() + self.wait_until_visible('#empty-state-customimagestable') + + # Check message when no custom image + self.assertTrue( + "You have not created any custom images yet." in str( + self.find('#empty-state-customimagestable').text + ) + ) + div_empty_msg = self.find('#empty-state-customimagestable') + link_create_custom_image = div_empty_msg.find_element( + By.TAG_NAME, 'a') + self.assertTrue(TestProjectConfigTab.project_id is not None) + self.assertTrue( + f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str( + link_create_custom_image.get_attribute('href') + ) + ) + self.assertTrue( + "Create your first custom image" in str( + link_create_custom_image.text + ) + ) + TestProjectConfigTab.project_id = None # reset project id + + def test_project_page_image_recipe(self): + """ Test project page section images + - Check image recipes are displayed + - Check search input + - Check image recipe build button works + - Check image recipe table features(show/hide column, pagination) + """ + self._navigate_to_project_page() + # navigate to "Images section" + images_section = self._get_config_nav_item(3) + images_section.click() + self.wait_until_visible('#imagerecipestable') + rows = self.find_all('#imagerecipestable tbody tr') + self.assertTrue(len(rows) > 0) + + # Test search input + self.wait_until_visible('#search-input-imagerecipestable') + recipe_input = self.find('#search-input-imagerecipestable') + recipe_input.send_keys('core-image-minimal') + self.find('#search-submit-imagerecipestable').click() + self.wait_until_visible('#imagerecipestable tbody tr') + rows = self.find_all('#imagerecipestable tbody tr') + self.assertTrue(len(rows) > 0) diff --git a/lib/toaster/tests/functional/utils.py b/lib/toaster/tests/functional/utils.py new file mode 100644 index 000000000..7269fa180 --- /dev/null +++ b/lib/toaster/tests/functional/utils.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only + + +from time import sleep +from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException +from selenium.webdriver.common.by import By + +from orm.models import Build + + +def wait_until_build(test_instance, state): + timeout = 60 + start_time = 0 + build_state = '' + while True: + try: + if start_time > timeout: + raise TimeoutException( + f'Build did not reach {state} state within {timeout} seconds' + ) + last_build_state = test_instance.driver.find_element( + By.XPATH, + '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', + ) + build_state = last_build_state.get_attribute( + 'data-build-state') + state_text = state.lower().split() + if any(x in str(build_state).lower() for x in state_text): + return str(build_state).lower() + if 'failed' in str(build_state).lower(): + break + except NoSuchElementException: + continue + except TimeoutException: + break + start_time += 1 + sleep(1) # take a breath and try again + +def wait_until_build_cancelled(test_instance): + """ Cancel build take a while sometime, the method is to wait driver action + until build being cancelled + """ + timeout = 30 + start_time = 0 + build = None + while True: + try: + if start_time > timeout: + raise TimeoutException( + f'Build did not reach cancelled state within {timeout} seconds' + ) + last_build_state = test_instance.driver.find_element( + By.XPATH, + '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', + ) + build_state = last_build_state.get_attribute( + 'data-build-state') + if 'failed' in str(build_state).lower(): + break + if 'cancelling' in str(build_state).lower(): + # Change build state to cancelled + if not build: # get build object only once + build = Build.objects.last() + build.outcome = Build.CANCELLED + build.save() + if 'cancelled' in str(build_state).lower(): + break + except NoSuchElementException: + continue + except StaleElementReferenceException: + continue + except TimeoutException: + break + start_time += 1 + sleep(1) # take a breath and try again + +def get_projectId_from_url(url): + # url = 'http://domainename.com/toastergui/project/1656/whatever + # or url = 'http://domainename.com/toastergui/project/1/ + # or url = 'http://domainename.com/toastergui/project/186 + assert '/toastergui/project/' in url, "URL is not valid" + url_to_list = url.split('/toastergui/project/') + return int(url_to_list[1].split('/')[0]) # project_id diff --git a/lib/toaster/tests/toaster-tests-requirements.txt b/lib/toaster/tests/toaster-tests-requirements.txt index 4f9fcc46d..71cc08343 100644 --- a/lib/toaster/tests/toaster-tests-requirements.txt +++ b/lib/toaster/tests/toaster-tests-requirements.txt @@ -1 +1,7 @@ -selenium==2.49.2 +selenium>=4.13.0 +pytest==7.4.2 +pytest-django==4.5.2 +pytest-env==1.1.0 +pytest-html==4.0.2 +pytest-metadata==3.0.0 +pytest-order==1.1.0 diff --git a/lib/toaster/tests/views/test_views.py b/lib/toaster/tests/views/test_views.py index 477654ea5..e1adfcf86 100644 --- a/lib/toaster/tests/views/test_views.py +++ b/lib/toaster/tests/views/test_views.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 # # BitBake Toaster Implementation # @@ -9,9 +9,11 @@ """Test cases for Toaster GUI and ReST.""" +import os +import pytest from django.test import TestCase from django.test.client import RequestFactory -from django.core.urlresolvers import reverse +from django.urls import reverse from django.db.models import Q from orm.models import Project, Package @@ -19,6 +21,7 @@ from orm.models import Layer_Version, Recipe from orm.models import CustomImageRecipe from orm.models import CustomImagePackage +from bldcontrol.models import BuildEnvironment import inspect import toastergui @@ -32,19 +35,32 @@ PROJECT_NAME2 = "test project 2" CLI_BUILDS_PROJECT_NAME = 'Command line builds' + class ViewTests(TestCase): """Tests to verify view APIs.""" fixtures = ['toastergui-unittest-data'] + builldir = os.environ.get('BUILDDIR') def setUp(self): self.project = Project.objects.first() + self.recipe1 = Recipe.objects.get(pk=2) + # create a file and to recipe1 file_path + file_path = f"{self.builldir}/{self.recipe1.name.strip().replace(' ', '-')}.bb" + with open(file_path, 'w') as f: + f.write('foo') + self.recipe1.file_path = file_path + self.recipe1.save() + self.customr = CustomImageRecipe.objects.first() self.cust_package = CustomImagePackage.objects.first() self.package = Package.objects.first() self.lver = Layer_Version.objects.first() + if BuildEnvironment.objects.count() == 0: + BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL) + def test_get_base_call_returns_html(self): """Basic test for all-projects view""" @@ -226,7 +242,7 @@ class ViewTests(TestCase): recipe = CustomImageRecipe.objects.create( name=name, project=self.project, base_recipe=self.recipe1, - file_path="/tmp/testing", + file_path=f"{self.builldir}/testing", layer_version=self.customr.layer_version) url = reverse('xhr_customrecipe_id', args=(recipe.id,)) response = self.client.delete(url) @@ -297,7 +313,7 @@ class ViewTests(TestCase): """Download the recipe file generated for the custom image""" # Create a dummy recipe file for the custom image generation to read - open("/tmp/a_recipe.bb", 'a').close() + open(f"{self.builldir}/a_recipe.bb", 'a').close() response = self.client.get(reverse('customrecipedownload', args=(self.project.id, self.customr.id))) |