Complete Yocto mirror with license table for TQMa6UL (2038-compliance)
- 264 license table entries with exact download URLs (224/264 resolved) - Complete sources/ directory with all BitBake recipes - Build configuration: tqma6ul-multi-mba6ulx, spaetzle (musl) - Full traceability for Softwarefreigabeantrag - GCC 13.4.0, Linux 6.6.102, U-Boot 2023.04, musl 1.2.4 - License distribution: GPL-2.0 (24), MIT (23), GPL-2.0+ (18), BSD-3 (16)
This commit is contained in:
0
sources/poky/bitbake/lib/toaster/__init__.py
Normal file
0
sources/poky/bitbake/lib/toaster/__init__.py
Normal file
37
sources/poky/bitbake/lib/toaster/bldcollector/admin.py
Normal file
37
sources/poky/bitbake/lib/toaster/bldcollector/admin.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.contrib import admin
|
||||
from orm.models import BitbakeVersion, Release, ToasterSetting, Layer_Version
|
||||
from django import forms
|
||||
import django.db.models as models
|
||||
|
||||
|
||||
class BitbakeVersionAdmin(admin.ModelAdmin):
|
||||
|
||||
# we override the formfield for db URLField
|
||||
# because of broken URL validation
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if isinstance(db_field, models.fields.URLField):
|
||||
return forms.fields.CharField()
|
||||
return super(BitbakeVersionAdmin, self).formfield_for_dbfield(
|
||||
db_field, **kwargs)
|
||||
|
||||
|
||||
class ReleaseAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class ToasterSettingAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class LayerVersionsAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
admin.site.register(Layer_Version, LayerVersionsAdmin)
|
||||
admin.site.register(BitbakeVersion, BitbakeVersionAdmin)
|
||||
admin.site.register(Release, ReleaseAdmin)
|
||||
admin.site.register(ToasterSetting, ToasterSettingAdmin)
|
||||
16
sources/poky/bitbake/lib/toaster/bldcollector/urls.py
Normal file
16
sources/poky/bitbake/lib/toaster/bldcollector/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2014-2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.urls import re_path as url
|
||||
|
||||
import bldcollector.views
|
||||
|
||||
urlpatterns = [
|
||||
# landing point for pushing a bitbake_eventlog.json file to this toaster instace
|
||||
url(r'^eventfile$', bldcollector.views.eventfile, name='eventfile'),
|
||||
]
|
||||
47
sources/poky/bitbake/lib/toaster/bldcollector/views.py
Normal file
47
sources/poky/bitbake/lib/toaster/bldcollector/views.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2014 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseBadRequest, HttpResponse
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import toastermain
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from toastermain.logs import log_view_mixin
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@log_view_mixin
|
||||
def eventfile(request):
|
||||
""" Receives a file by POST, and runs toaster-eventreply on this file """
|
||||
if request.method != "POST":
|
||||
return HttpResponseBadRequest("This API only accepts POST requests. Post a file with:\n\ncurl -F eventlog=@bitbake_eventlog.json %s\n" % request.build_absolute_uri(reverse('eventfile')), content_type="text/plain;utf8")
|
||||
|
||||
# write temporary file
|
||||
(handle, abstemppath) = tempfile.mkstemp(dir="/tmp/")
|
||||
with os.fdopen(handle, "w") as tmpfile:
|
||||
for chunk in request.FILES['eventlog'].chunks():
|
||||
tmpfile.write(chunk)
|
||||
tmpfile.close()
|
||||
|
||||
# compute the path to "bitbake/bin/toaster-eventreplay"
|
||||
from os.path import dirname as DN
|
||||
import_script = os.path.join(DN(DN(DN(DN(os.path.abspath(__file__))))), "bin/toaster-eventreplay")
|
||||
if not os.path.exists(import_script):
|
||||
raise Exception("script missing %s" % import_script)
|
||||
scriptenv = os.environ.copy()
|
||||
scriptenv["DATABASE_URL"] = toastermain.settings.getDATABASE_URL()
|
||||
|
||||
# run the data loading process and return the results
|
||||
importer = subprocess.Popen([import_script, abstemppath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=scriptenv)
|
||||
(out, err) = importer.communicate()
|
||||
if importer.returncode == 0:
|
||||
os.remove(abstemppath)
|
||||
return HttpResponse("== Retval %d\n== STDOUT\n%s\n\n== STDERR\n%s" % (importer.returncode, out, err), content_type="text/plain;utf8")
|
||||
11
sources/poky/bitbake/lib/toaster/bldcontrol/admin.py
Normal file
11
sources/poky/bitbake/lib/toaster/bldcontrol/admin.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import BuildEnvironment
|
||||
|
||||
class BuildEnvironmentAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
admin.site.register(BuildEnvironment, BuildEnvironmentAdmin)
|
||||
126
sources/poky/bitbake/lib/toaster/bldcontrol/bbcontroller.py
Normal file
126
sources/poky/bitbake/lib/toaster/bldcontrol/bbcontroller.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2014 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
from django.db.models import Q
|
||||
from bldcontrol.models import BuildEnvironment, BRLayer, BRBitbake
|
||||
|
||||
# load Bitbake components
|
||||
path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
sys.path.insert(0, path)
|
||||
|
||||
class BitbakeController(object):
|
||||
""" This is the basic class that controlls a bitbake server.
|
||||
It is outside the scope of this class on how the server is started and aquired
|
||||
"""
|
||||
|
||||
def __init__(self, be):
|
||||
import bb.server.xmlrpcclient
|
||||
self.connection = bb.server.xmlrpcclient._create_server(be.bbaddress,
|
||||
int(be.bbport))[0]
|
||||
|
||||
def _runCommand(self, command):
|
||||
result, error = self.connection.runCommand(command)
|
||||
if error:
|
||||
raise Exception(error)
|
||||
return result
|
||||
|
||||
def disconnect(self):
|
||||
return self.connection.removeClient()
|
||||
|
||||
def setVariable(self, name, value):
|
||||
return self._runCommand(["setVariable", name, value])
|
||||
|
||||
def getVariable(self, name):
|
||||
return self._runCommand(["getVariable", name])
|
||||
|
||||
def triggerEvent(self, event):
|
||||
return self._runCommand(["triggerEvent", event])
|
||||
|
||||
def build(self, targets, task = None):
|
||||
if task is None:
|
||||
task = "build"
|
||||
return self._runCommand(["buildTargets", targets, task])
|
||||
|
||||
def forceShutDown(self):
|
||||
return self._runCommand(["stateForceShutdown"])
|
||||
|
||||
|
||||
|
||||
def getBuildEnvironmentController(**kwargs):
|
||||
""" Gets you a BuildEnvironmentController that encapsulates a build environment,
|
||||
based on the query dictionary sent in.
|
||||
|
||||
This is used to retrieve, for example, the currently running BE from inside
|
||||
the toaster UI, or find a new BE to start a new build in it.
|
||||
|
||||
The return object MUST always be a BuildEnvironmentController.
|
||||
"""
|
||||
|
||||
from bldcontrol.localhostbecontroller import LocalhostBEController
|
||||
|
||||
be = BuildEnvironment.objects.filter(Q(**kwargs))[0]
|
||||
if be.betype == BuildEnvironment.TYPE_LOCAL:
|
||||
return LocalhostBEController(be)
|
||||
else:
|
||||
raise Exception("FIXME: Implement BEC for type %s" % str(be.betype))
|
||||
|
||||
|
||||
class BuildEnvironmentController(object):
|
||||
""" BuildEnvironmentController (BEC) is the abstract class that defines the operations that MUST
|
||||
or SHOULD be supported by a Build Environment. It is used to establish the framework, and must
|
||||
not be instantiated directly by the user.
|
||||
|
||||
Use the "getBuildEnvironmentController()" function to get a working BEC for your remote.
|
||||
|
||||
How the BuildEnvironments are discovered is outside the scope of this class.
|
||||
|
||||
You must derive this class to teach Toaster how to operate in your own infrastructure.
|
||||
We provide some specific BuildEnvironmentController classes that can be used either to
|
||||
directly set-up Toaster infrastructure, or as a model for your own infrastructure set:
|
||||
|
||||
* Localhost controller will run the Toaster BE on the same account as the web server
|
||||
(current user if you are using the the Django development web server)
|
||||
on the local machine, with the "build/" directory under the "poky/" source checkout directory.
|
||||
Bash is expected to be available.
|
||||
|
||||
"""
|
||||
def __init__(self, be):
|
||||
""" Takes a BuildEnvironment object as parameter that points to the settings of the BE.
|
||||
"""
|
||||
self.be = be
|
||||
self.connection = None
|
||||
|
||||
def setLayers(self, bitbake, ls):
|
||||
""" Checks-out bitbake executor and layers from git repositories.
|
||||
Sets the layer variables in the config file, after validating local layer paths.
|
||||
bitbake must be a single BRBitbake instance
|
||||
The layer paths must be in a list of BRLayer object
|
||||
|
||||
a word of attention: by convention, the first layer for any build will be poky!
|
||||
"""
|
||||
raise NotImplementedError("FIXME: Must override setLayers")
|
||||
|
||||
def getArtifact(self, path):
|
||||
""" This call returns an artifact identified by the 'path'. How 'path' is interpreted as
|
||||
up to the implementing BEC. The return MUST be a REST URL where a GET will actually return
|
||||
the content of the artifact, e.g. for use as a "download link" in a web UI.
|
||||
"""
|
||||
raise NotImplementedError("Must return the REST URL of the artifact")
|
||||
|
||||
def triggerBuild(self, bitbake, layers, variables, targets):
|
||||
raise NotImplementedError("Must override BE release")
|
||||
|
||||
class ShellCmdException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BuildSetupException(Exception):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2014 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
from bldcontrol.models import BuildEnvironment, BuildRequest, Build
|
||||
from orm.models import CustomImageRecipe, Layer, Layer_Version, Project, ToasterSetting
|
||||
from orm.models import signal_runbuilds
|
||||
import subprocess
|
||||
|
||||
from toastermain import settings
|
||||
|
||||
from bldcontrol.bbcontroller import BuildEnvironmentController, ShellCmdException, BuildSetupException
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
install_dir = os.environ.get('TOASTER_DIR')
|
||||
|
||||
from pprint import pformat
|
||||
|
||||
class LocalhostBEController(BuildEnvironmentController):
|
||||
""" Implementation of the BuildEnvironmentController for the localhost;
|
||||
this controller manages the default build directory,
|
||||
the server setup and system start and stop for the localhost-type build environment
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, be):
|
||||
super(LocalhostBEController, self).__init__(be)
|
||||
self.pokydirname = None
|
||||
self.islayerset = False
|
||||
|
||||
def _shellcmd(self, command, cwd=None, nowait=False,env=None):
|
||||
if cwd is None:
|
||||
cwd = self.be.sourcedir
|
||||
if env is None:
|
||||
env=os.environ.copy()
|
||||
|
||||
logger.debug("lbc_shellcmd: (%s) %s" % (cwd, command))
|
||||
p = subprocess.Popen(command, cwd = cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||
if nowait:
|
||||
return
|
||||
(out,err) = p.communicate()
|
||||
p.wait()
|
||||
if p.returncode:
|
||||
if len(err) == 0:
|
||||
err = "command: %s \n%s" % (command, out)
|
||||
else:
|
||||
err = "command: %s \n%s" % (command, err)
|
||||
logger.warning("localhostbecontroller: shellcmd error %s" % err)
|
||||
raise ShellCmdException(err)
|
||||
else:
|
||||
logger.debug("localhostbecontroller: shellcmd success")
|
||||
return out.decode('utf-8')
|
||||
|
||||
def getGitCloneDirectory(self, url, branch):
|
||||
"""Construct unique clone directory name out of url and branch."""
|
||||
if branch != "HEAD":
|
||||
return "_toaster_clones/_%s_%s" % (re.sub('[:/@+%]', '_', url), branch)
|
||||
|
||||
# word of attention; this is a localhost-specific issue; only on the localhost we expect to have "HEAD" releases
|
||||
# which _ALWAYS_ means the current poky checkout
|
||||
from os.path import dirname as DN
|
||||
local_checkout_path = DN(DN(DN(DN(DN(os.path.abspath(__file__))))))
|
||||
#logger.debug("localhostbecontroller: using HEAD checkout in %s" % local_checkout_path)
|
||||
return local_checkout_path
|
||||
|
||||
def setCloneStatus(self,bitbake,status,total,current,repo_name):
|
||||
bitbake.req.build.repos_cloned=current
|
||||
bitbake.req.build.repos_to_clone=total
|
||||
bitbake.req.build.progress_item=repo_name
|
||||
bitbake.req.build.save()
|
||||
|
||||
def setLayers(self, bitbake, layers, targets):
|
||||
""" a word of attention: by convention, the first layer for any build will be poky! """
|
||||
|
||||
assert self.be.sourcedir is not None
|
||||
|
||||
layerlist = []
|
||||
nongitlayerlist = []
|
||||
layer_index = 0
|
||||
git_env = os.environ.copy()
|
||||
# (note: add custom environment settings here)
|
||||
|
||||
# set layers in the layersource
|
||||
|
||||
# 1. get a list of repos with branches, and map dirpaths for each layer
|
||||
gitrepos = {}
|
||||
|
||||
# if we're using a remotely fetched version of bitbake add its git
|
||||
# details to the list of repos to clone
|
||||
if bitbake.giturl and bitbake.commit:
|
||||
gitrepos[(bitbake.giturl, bitbake.commit)] = []
|
||||
gitrepos[(bitbake.giturl, bitbake.commit)].append(
|
||||
("bitbake", bitbake.dirpath, 0))
|
||||
|
||||
for layer in layers:
|
||||
# We don't need to git clone the layer for the CustomImageRecipe
|
||||
# as it's generated by us layer on if needed
|
||||
if CustomImageRecipe.LAYER_NAME in layer.name:
|
||||
continue
|
||||
|
||||
# If we have local layers then we don't need clone them
|
||||
# For local layers giturl will be empty
|
||||
if not layer.giturl:
|
||||
nongitlayerlist.append( "%03d:%s" % (layer_index,layer.local_source_dir) )
|
||||
continue
|
||||
|
||||
if not (layer.giturl, layer.commit) in gitrepos:
|
||||
gitrepos[(layer.giturl, layer.commit)] = []
|
||||
gitrepos[(layer.giturl, layer.commit)].append( (layer.name,layer.dirpath,layer_index) )
|
||||
layer_index += 1
|
||||
|
||||
|
||||
logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos))
|
||||
|
||||
|
||||
# 2. Note for future use if the current source directory is a
|
||||
# checked-out git repos that could match a layer's vcs_url and therefore
|
||||
# be used to speed up cloning (rather than fetching it again).
|
||||
|
||||
cached_layers = {}
|
||||
|
||||
try:
|
||||
for remotes in self._shellcmd("git remote -v", self.be.sourcedir,env=git_env).split("\n"):
|
||||
try:
|
||||
remote = remotes.split("\t")[1].split(" ")[0]
|
||||
if remote not in cached_layers:
|
||||
cached_layers[remote] = self.be.sourcedir
|
||||
except IndexError:
|
||||
pass
|
||||
except ShellCmdException:
|
||||
# ignore any errors in collecting git remotes this is an optional
|
||||
# step
|
||||
pass
|
||||
|
||||
logger.info("Using pre-checked out source for layer %s", cached_layers)
|
||||
|
||||
# 3. checkout the repositories
|
||||
clone_count=0
|
||||
clone_total=len(gitrepos.keys())
|
||||
self.setCloneStatus(bitbake,'Started',clone_total,clone_count,'')
|
||||
for giturl, commit in gitrepos.keys():
|
||||
self.setCloneStatus(bitbake,'progress',clone_total,clone_count,gitrepos[(giturl, commit)][0][0])
|
||||
clone_count += 1
|
||||
|
||||
localdirname = os.path.join(self.be.sourcedir, self.getGitCloneDirectory(giturl, commit))
|
||||
logger.debug("localhostbecontroller: giturl %s:%s checking out in current directory %s" % (giturl, commit, localdirname))
|
||||
|
||||
# see if our directory is a git repository
|
||||
if os.path.exists(localdirname):
|
||||
try:
|
||||
localremotes = self._shellcmd("git remote -v",
|
||||
localdirname,env=git_env)
|
||||
# NOTE: this nice-to-have check breaks when using git remaping to get past firewall
|
||||
# Re-enable later with .gitconfig remapping checks
|
||||
#if not giturl in localremotes and commit != 'HEAD':
|
||||
# raise BuildSetupException("Existing git repository at %s, but with different remotes ('%s', expected '%s'). Toaster will not continue out of fear of damaging something." % (localdirname, ", ".join(localremotes.split("\n")), giturl))
|
||||
pass
|
||||
except ShellCmdException:
|
||||
# our localdirname might not be a git repository
|
||||
#- that's fine
|
||||
pass
|
||||
else:
|
||||
if giturl in cached_layers:
|
||||
logger.debug("localhostbecontroller git-copying %s to %s" % (cached_layers[giturl], localdirname))
|
||||
self._shellcmd("git clone \"%s\" \"%s\"" % (cached_layers[giturl], localdirname),env=git_env)
|
||||
self._shellcmd("git remote remove origin", localdirname,env=git_env)
|
||||
self._shellcmd("git remote add origin \"%s\"" % giturl, localdirname,env=git_env)
|
||||
else:
|
||||
logger.debug("localhostbecontroller: cloning %s in %s" % (giturl, localdirname))
|
||||
self._shellcmd('git clone "%s" "%s"' % (giturl, localdirname),env=git_env)
|
||||
|
||||
# branch magic name "HEAD" will inhibit checkout
|
||||
if commit != "HEAD":
|
||||
logger.debug("localhostbecontroller: checking out commit %s to %s " % (commit, localdirname))
|
||||
ref = commit if re.match('^[a-fA-F0-9]+$', commit) else 'origin/%s' % commit
|
||||
self._shellcmd('git fetch && git reset --hard "%s"' % ref, localdirname,env=git_env)
|
||||
|
||||
# take the localdirname as poky dir if we can find the oe-init-build-env
|
||||
if self.pokydirname is None and os.path.exists(os.path.join(localdirname, "oe-init-build-env")):
|
||||
logger.debug("localhostbecontroller: selected poky dir name %s" % localdirname)
|
||||
self.pokydirname = localdirname
|
||||
|
||||
# make sure we have a working bitbake
|
||||
if not os.path.exists(os.path.join(self.pokydirname, 'bitbake')):
|
||||
logger.debug("localhostbecontroller: checking bitbake into the poky dirname %s " % self.pokydirname)
|
||||
self._shellcmd("git clone -b \"%s\" \"%s\" \"%s\" " % (bitbake.commit, bitbake.giturl, os.path.join(self.pokydirname, 'bitbake')),env=git_env)
|
||||
|
||||
# verify our repositories
|
||||
for name, dirpath, index in gitrepos[(giturl, commit)]:
|
||||
localdirpath = os.path.join(localdirname, dirpath)
|
||||
logger.debug("localhostbecontroller: localdirpath expects '%s'" % localdirpath)
|
||||
if not os.path.exists(localdirpath):
|
||||
raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Exiting." % (localdirpath, giturl, commit))
|
||||
|
||||
if name != "bitbake":
|
||||
layerlist.append("%03d:%s" % (index,localdirpath.rstrip("/")))
|
||||
|
||||
self.setCloneStatus(bitbake,'complete',clone_total,clone_count,'')
|
||||
logger.debug("localhostbecontroller: current layer list %s " % pformat(layerlist))
|
||||
|
||||
# Resolve self.pokydirname if not resolved yet, consider the scenario
|
||||
# where all layers are local, that's the else clause
|
||||
if self.pokydirname is None:
|
||||
if os.path.exists(os.path.join(self.be.sourcedir, "oe-init-build-env")):
|
||||
logger.debug("localhostbecontroller: selected poky dir name %s" % self.be.sourcedir)
|
||||
self.pokydirname = self.be.sourcedir
|
||||
else:
|
||||
# Alternatively, scan local layers for relative "oe-init-build-env" location
|
||||
for layer in layers:
|
||||
if os.path.exists(os.path.join(layer.layer_version.layer.local_source_dir,"..","oe-init-build-env")):
|
||||
logger.debug("localhostbecontroller, setting pokydirname to %s" % (layer.layer_version.layer.local_source_dir))
|
||||
self.pokydirname = os.path.join(layer.layer_version.layer.local_source_dir,"..")
|
||||
break
|
||||
else:
|
||||
logger.error("pokydirname is not set, you will run into trouble!")
|
||||
|
||||
# 5. create custom layer and add custom recipes to it
|
||||
for target in targets:
|
||||
try:
|
||||
customrecipe = CustomImageRecipe.objects.get(
|
||||
name=target.target,
|
||||
project=bitbake.req.project)
|
||||
|
||||
custom_layer_path = self.setup_custom_image_recipe(
|
||||
customrecipe, layers)
|
||||
|
||||
if os.path.isdir(custom_layer_path):
|
||||
layerlist.append("%03d:%s" % (layer_index,custom_layer_path))
|
||||
|
||||
except CustomImageRecipe.DoesNotExist:
|
||||
continue # not a custom recipe, skip
|
||||
|
||||
layerlist.extend(nongitlayerlist)
|
||||
logger.debug("\n\nset layers gives this list %s" % pformat(layerlist))
|
||||
self.islayerset = True
|
||||
|
||||
# restore the order of layer list for bblayers.conf
|
||||
layerlist.sort()
|
||||
sorted_layerlist = [l[4:] for l in layerlist]
|
||||
return sorted_layerlist
|
||||
|
||||
def setup_custom_image_recipe(self, customrecipe, layers):
|
||||
""" Set up toaster-custom-images layer and recipe files """
|
||||
layerpath = os.path.join(self.be.builddir,
|
||||
CustomImageRecipe.LAYER_NAME)
|
||||
|
||||
# create directory structure
|
||||
for name in ("conf", "recipes"):
|
||||
path = os.path.join(layerpath, name)
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
|
||||
# create layer.conf
|
||||
config = os.path.join(layerpath, "conf", "layer.conf")
|
||||
if not os.path.isfile(config):
|
||||
with open(config, "w") as conf:
|
||||
conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
|
||||
|
||||
# Update the Layer_Version dirpath that has our base_recipe in
|
||||
# to be able to read the base recipe to then generate the
|
||||
# custom recipe.
|
||||
br_layer_base_recipe = layers.get(
|
||||
layer_version=customrecipe.base_recipe.layer_version)
|
||||
|
||||
# If the layer is one that we've cloned we know where it lives
|
||||
if br_layer_base_recipe.giturl and br_layer_base_recipe.commit:
|
||||
layer_path = self.getGitCloneDirectory(
|
||||
br_layer_base_recipe.giturl,
|
||||
br_layer_base_recipe.commit)
|
||||
# Otherwise it's a local layer
|
||||
elif br_layer_base_recipe.local_source_dir:
|
||||
layer_path = br_layer_base_recipe.local_source_dir
|
||||
else:
|
||||
logger.error("Unable to workout the dir path for the custom"
|
||||
" image recipe")
|
||||
|
||||
br_layer_base_dirpath = os.path.join(
|
||||
self.be.sourcedir,
|
||||
layer_path,
|
||||
customrecipe.base_recipe.layer_version.dirpath)
|
||||
|
||||
customrecipe.base_recipe.layer_version.dirpath = br_layer_base_dirpath
|
||||
|
||||
customrecipe.base_recipe.layer_version.save()
|
||||
|
||||
# create recipe
|
||||
recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
|
||||
customrecipe.name)
|
||||
with open(recipe_path, "w") as recipef:
|
||||
recipef.write(customrecipe.generate_recipe_file_contents())
|
||||
|
||||
# Update the layer and recipe objects
|
||||
customrecipe.layer_version.dirpath = layerpath
|
||||
customrecipe.layer_version.layer.local_source_dir = layerpath
|
||||
customrecipe.layer_version.layer.save()
|
||||
customrecipe.layer_version.save()
|
||||
|
||||
customrecipe.file_path = recipe_path
|
||||
customrecipe.save()
|
||||
|
||||
return layerpath
|
||||
|
||||
|
||||
def readServerLogFile(self):
|
||||
return open(os.path.join(self.be.builddir, "toaster_server.log"), "r").read()
|
||||
|
||||
|
||||
def triggerBuild(self, bitbake, layers, variables, targets, brbe):
|
||||
layers = self.setLayers(bitbake, layers, targets)
|
||||
is_merged_attr = bitbake.req.project.merged_attr
|
||||
|
||||
git_env = os.environ.copy()
|
||||
# (note: add custom environment settings here)
|
||||
try:
|
||||
# insure that the project init/build uses the selected bitbake, and not Toaster's
|
||||
del git_env['TEMPLATECONF']
|
||||
del git_env['BBBASEDIR']
|
||||
del git_env['BUILDDIR']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# init build environment from the clone
|
||||
if bitbake.req.project.builddir:
|
||||
builddir = bitbake.req.project.builddir
|
||||
else:
|
||||
builddir = '%s-toaster-%d' % (self.be.builddir, bitbake.req.project.id)
|
||||
oe_init = os.path.join(self.pokydirname, 'oe-init-build-env')
|
||||
# init build environment
|
||||
try:
|
||||
custom_script = ToasterSetting.objects.get(name="CUSTOM_BUILD_INIT_SCRIPT").value
|
||||
custom_script = custom_script.replace("%BUILDDIR%" ,builddir)
|
||||
self._shellcmd("bash -c 'source %s'" % (custom_script),env=git_env)
|
||||
except ToasterSetting.DoesNotExist:
|
||||
self._shellcmd("bash -c 'source %s %s'" % (oe_init, builddir),
|
||||
self.be.sourcedir,env=git_env)
|
||||
|
||||
# update bblayers.conf
|
||||
if not is_merged_attr:
|
||||
bblconfpath = os.path.join(builddir, "conf/toaster-bblayers.conf")
|
||||
with open(bblconfpath, 'w') as bblayers:
|
||||
bblayers.write('# line added by toaster build control\n'
|
||||
'BBLAYERS = "%s"' % ' '.join(layers))
|
||||
|
||||
# write configuration file
|
||||
confpath = os.path.join(builddir, 'conf/toaster.conf')
|
||||
with open(confpath, 'w') as conf:
|
||||
for var in variables:
|
||||
conf.write('%s="%s"\n' % (var.name, var.value))
|
||||
conf.write('INHERIT+="toaster buildhistory"')
|
||||
else:
|
||||
# Append the Toaster-specific values directly to the bblayers.conf
|
||||
bblconfpath = os.path.join(builddir, "conf/bblayers.conf")
|
||||
bblconfpath_save = os.path.join(builddir, "conf/bblayers.conf.save")
|
||||
shutil.copyfile(bblconfpath, bblconfpath_save)
|
||||
with open(bblconfpath) as bblayers:
|
||||
content = bblayers.readlines()
|
||||
do_write = True
|
||||
was_toaster = False
|
||||
with open(bblconfpath,'w') as bblayers:
|
||||
for line in content:
|
||||
#line = line.strip('\n')
|
||||
if 'TOASTER_CONFIG_PROLOG' in line:
|
||||
do_write = False
|
||||
was_toaster = True
|
||||
elif 'TOASTER_CONFIG_EPILOG' in line:
|
||||
do_write = True
|
||||
elif do_write:
|
||||
bblayers.write(line)
|
||||
if not was_toaster:
|
||||
bblayers.write('\n')
|
||||
bblayers.write('#=== TOASTER_CONFIG_PROLOG ===\n')
|
||||
bblayers.write('BBLAYERS = "\\\n')
|
||||
for layer in layers:
|
||||
bblayers.write(' %s \\\n' % layer)
|
||||
bblayers.write(' "\n')
|
||||
bblayers.write('#=== TOASTER_CONFIG_EPILOG ===\n')
|
||||
# Append the Toaster-specific values directly to the local.conf
|
||||
bbconfpath = os.path.join(builddir, "conf/local.conf")
|
||||
bbconfpath_save = os.path.join(builddir, "conf/local.conf.save")
|
||||
shutil.copyfile(bbconfpath, bbconfpath_save)
|
||||
with open(bbconfpath) as f:
|
||||
content = f.readlines()
|
||||
do_write = True
|
||||
was_toaster = False
|
||||
with open(bbconfpath,'w') as conf:
|
||||
for line in content:
|
||||
#line = line.strip('\n')
|
||||
if 'TOASTER_CONFIG_PROLOG' in line:
|
||||
do_write = False
|
||||
was_toaster = True
|
||||
elif 'TOASTER_CONFIG_EPILOG' in line:
|
||||
do_write = True
|
||||
elif do_write:
|
||||
conf.write(line)
|
||||
if not was_toaster:
|
||||
conf.write('\n')
|
||||
conf.write('#=== TOASTER_CONFIG_PROLOG ===\n')
|
||||
for var in variables:
|
||||
if (not var.name.startswith("INTERNAL_")) and (not var.name == "BBLAYERS"):
|
||||
conf.write('%s="%s"\n' % (var.name, var.value))
|
||||
conf.write('#=== TOASTER_CONFIG_EPILOG ===\n')
|
||||
|
||||
# If 'target' is just the project preparation target, then we are done
|
||||
for target in targets:
|
||||
if "_PROJECT_PREPARE_" == target.target:
|
||||
logger.debug('localhostbecontroller: Project has been prepared. Done.')
|
||||
# Update the Build Request and release the build environment
|
||||
bitbake.req.state = BuildRequest.REQ_COMPLETED
|
||||
bitbake.req.save()
|
||||
self.be.lock = BuildEnvironment.LOCK_FREE
|
||||
self.be.save()
|
||||
# Close the project build and progress bar
|
||||
bitbake.req.build.outcome = Build.SUCCEEDED
|
||||
bitbake.req.build.save()
|
||||
# Update the project status
|
||||
bitbake.req.project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_CLONING_SUCCESS)
|
||||
signal_runbuilds()
|
||||
return
|
||||
|
||||
# clean the Toaster to build environment
|
||||
env_clean = 'unset BBPATH;' # clean BBPATH for <= YP-2.4.0
|
||||
|
||||
# run bitbake server from the clone if available
|
||||
# otherwise pick it from the PATH
|
||||
bitbake = os.path.join(self.pokydirname, 'bitbake', 'bin', 'bitbake')
|
||||
if not os.path.exists(bitbake):
|
||||
logger.info("Bitbake not available under %s, will try to use it from PATH" %
|
||||
self.pokydirname)
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
if os.path.exists(os.path.join(path, 'bitbake')):
|
||||
bitbake = os.path.join(path, 'bitbake')
|
||||
break
|
||||
else:
|
||||
logger.error("Looks like Bitbake is not available, please fix your environment")
|
||||
|
||||
toasterlayers = os.path.join(builddir,"conf/toaster-bblayers.conf")
|
||||
if not is_merged_attr:
|
||||
self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s --read %s --read %s '
|
||||
'--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
|
||||
builddir, bitbake, confpath, toasterlayers), self.be.sourcedir)
|
||||
else:
|
||||
self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s '
|
||||
'--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
|
||||
builddir, bitbake), self.be.sourcedir)
|
||||
|
||||
# read port number from bitbake.lock
|
||||
self.be.bbport = -1
|
||||
bblock = os.path.join(builddir, 'bitbake.lock')
|
||||
# allow 10 seconds for bb lock file to appear but also be populated
|
||||
for lock_check in range(10):
|
||||
if not os.path.exists(bblock):
|
||||
logger.debug("localhostbecontroller: waiting for bblock file to appear")
|
||||
time.sleep(1)
|
||||
continue
|
||||
if 10 < os.stat(bblock).st_size:
|
||||
break
|
||||
logger.debug("localhostbecontroller: waiting for bblock content to appear")
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise BuildSetupException("Cannot find bitbake server lock file '%s'. Exiting." % bblock)
|
||||
|
||||
with open(bblock) as fplock:
|
||||
for line in fplock:
|
||||
if ":" in line:
|
||||
self.be.bbport = line.split(":")[-1].strip()
|
||||
logger.debug("localhostbecontroller: bitbake port %s", self.be.bbport)
|
||||
break
|
||||
|
||||
if -1 == self.be.bbport:
|
||||
raise BuildSetupException("localhostbecontroller: can't read bitbake port from %s" % bblock)
|
||||
|
||||
self.be.bbaddress = "localhost"
|
||||
self.be.bbstate = BuildEnvironment.SERVER_STARTED
|
||||
self.be.lock = BuildEnvironment.LOCK_RUNNING
|
||||
self.be.save()
|
||||
|
||||
bbtargets = ''
|
||||
for target in targets:
|
||||
task = target.task
|
||||
if task:
|
||||
if not task.startswith('do_'):
|
||||
task = 'do_' + task
|
||||
task = ':%s' % task
|
||||
bbtargets += '%s%s ' % (target.target, task)
|
||||
|
||||
# run build with local bitbake. stop the server after the build.
|
||||
log = os.path.join(builddir, 'toaster_ui.log')
|
||||
local_bitbake = os.path.join(os.path.dirname(os.getenv('BBBASEDIR')),
|
||||
'bitbake')
|
||||
if not is_merged_attr:
|
||||
self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
|
||||
'%s %s -u toasterui --read %s --read %s --token="" >>%s 2>&1;'
|
||||
'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
|
||||
% (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, confpath, toasterlayers, log,
|
||||
self.be.bbport, bitbake,)],
|
||||
builddir, nowait=True)
|
||||
else:
|
||||
self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
|
||||
'%s %s -u toasterui --token="" >>%s 2>&1;'
|
||||
'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
|
||||
% (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, log,
|
||||
self.be.bbport, bitbake,)],
|
||||
builddir, nowait=True)
|
||||
|
||||
logger.debug('localhostbecontroller: Build launched, exiting. '
|
||||
'Follow build logs at %s' % log)
|
||||
@@ -0,0 +1,170 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django.core.management import call_command
|
||||
from bldcontrol.models import BuildRequest, BuildEnvironment, BRError
|
||||
from orm.models import ToasterSetting, Build, Layer
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
|
||||
def DN(path):
|
||||
if path is None:
|
||||
return ""
|
||||
else:
|
||||
return os.path.dirname(path)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
args = ""
|
||||
help = "Verifies that the configured settings are valid and usable, or prompts the user to fix the settings."
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Command, self).__init__(*args, **kwargs)
|
||||
self.guesspath = DN(DN(DN(DN(DN(DN(DN(__file__)))))))
|
||||
|
||||
def _verify_build_environment(self):
|
||||
# provide a local build env. This will be extended later to include non local
|
||||
if BuildEnvironment.objects.count() == 0:
|
||||
BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL)
|
||||
|
||||
# we make sure we have builddir and sourcedir for all defined build envionments
|
||||
for be in BuildEnvironment.objects.all():
|
||||
be.needs_import = False
|
||||
def _verify_be():
|
||||
is_changed = False
|
||||
|
||||
def _update_sourcedir():
|
||||
be.sourcedir = os.environ.get('TOASTER_DIR')
|
||||
return True
|
||||
|
||||
if len(be.sourcedir) == 0:
|
||||
is_changed = _update_sourcedir()
|
||||
|
||||
if not be.sourcedir.startswith("/"):
|
||||
print("\n -- Validation: The layers checkout directory must be set to an absolute path.")
|
||||
is_changed = _update_sourcedir()
|
||||
|
||||
if is_changed:
|
||||
if be.betype == BuildEnvironment.TYPE_LOCAL:
|
||||
be.needs_import = True
|
||||
return True
|
||||
|
||||
def _update_builddir():
|
||||
be.builddir = os.environ.get('TOASTER_DIR')+"/build"
|
||||
return True
|
||||
|
||||
if len(be.builddir) == 0:
|
||||
is_changed = _update_builddir()
|
||||
|
||||
if not be.builddir.startswith("/"):
|
||||
print("\n -- Validation: The build directory must to be set to an absolute path.")
|
||||
is_changed = _update_builddir()
|
||||
|
||||
if is_changed:
|
||||
print("\nBuild configuration saved")
|
||||
be.save()
|
||||
return True
|
||||
|
||||
if be.needs_import:
|
||||
try:
|
||||
print("Loading default settings")
|
||||
call_command("loaddata", "settings")
|
||||
template_conf = os.environ.get("TEMPLATECONF", "")
|
||||
custom_xml_only = os.environ.get("CUSTOM_XML_ONLY")
|
||||
|
||||
if ToasterSetting.objects.filter(name='CUSTOM_XML_ONLY').count() > 0 or custom_xml_only is not None:
|
||||
# only use the custom settings
|
||||
pass
|
||||
elif "poky" in template_conf:
|
||||
print("Loading poky configuration")
|
||||
call_command("loaddata", "poky")
|
||||
else:
|
||||
print("Loading OE-Core configuration")
|
||||
call_command("loaddata", "oe-core")
|
||||
if template_conf:
|
||||
oe_core_path = os.path.realpath(
|
||||
template_conf +
|
||||
"/../")
|
||||
else:
|
||||
print("TEMPLATECONF not found. You may have to"
|
||||
" manually configure layer paths")
|
||||
oe_core_path = input("Please enter the path of"
|
||||
" your openembedded-core "
|
||||
"layer: ")
|
||||
# Update the layer instances of openemebedded-core
|
||||
for layer in Layer.objects.filter(
|
||||
name="openembedded-core",
|
||||
local_source_dir="OE-CORE-LAYER-DIR"):
|
||||
layer.local_path = oe_core_path
|
||||
layer.save()
|
||||
|
||||
# Import the custom fixture if it's present
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
action="ignore",
|
||||
message="^.*No fixture named.*$")
|
||||
print("Importing custom settings if present")
|
||||
try:
|
||||
call_command("loaddata", "custom")
|
||||
except:
|
||||
print("NOTE: optional fixture 'custom' not found")
|
||||
|
||||
# we run lsupdates after config update
|
||||
print("\nFetching information from the layer index, "
|
||||
"please wait.\nYou can re-update any time later "
|
||||
"by running bitbake/lib/toaster/manage.py "
|
||||
"lsupdates\n")
|
||||
call_command("lsupdates")
|
||||
|
||||
# we don't look for any other config files
|
||||
return is_changed
|
||||
except Exception as e:
|
||||
print("Failure while trying to setup toaster: %s"
|
||||
% e)
|
||||
traceback.print_exc()
|
||||
|
||||
return is_changed
|
||||
|
||||
while _verify_be():
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _verify_default_settings(self):
|
||||
# verify that default settings are there
|
||||
if ToasterSetting.objects.filter(name='DEFAULT_RELEASE').count() != 1:
|
||||
ToasterSetting.objects.filter(name='DEFAULT_RELEASE').delete()
|
||||
ToasterSetting.objects.get_or_create(name='DEFAULT_RELEASE', value='')
|
||||
return 0
|
||||
|
||||
def _verify_builds_in_progress(self):
|
||||
# we are just starting up. we must not have any builds in progress, or build environments taken
|
||||
for b in BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS):
|
||||
BRError.objects.create(req=b, errtype="toaster",
|
||||
errmsg=
|
||||
"Toaster found this build IN PROGRESS while Toaster started up. This is an inconsistent state, and the build was marked as failed")
|
||||
|
||||
BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS).update(state=BuildRequest.REQ_FAILED)
|
||||
|
||||
BuildEnvironment.objects.update(lock=BuildEnvironment.LOCK_FREE)
|
||||
|
||||
# also mark "In Progress builds as failures"
|
||||
from django.utils import timezone
|
||||
Build.objects.filter(outcome=Build.IN_PROGRESS).update(outcome=Build.FAILED, completed_on=timezone.now())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
def handle(self, **options):
|
||||
retval = 0
|
||||
retval += self._verify_build_environment()
|
||||
retval += self._verify_default_settings()
|
||||
retval += self._verify_builds_in_progress()
|
||||
|
||||
return retval
|
||||
@@ -0,0 +1,275 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
|
||||
from bldcontrol.bbcontroller import getBuildEnvironmentController
|
||||
from bldcontrol.models import BuildRequest, BuildEnvironment
|
||||
from bldcontrol.models import BRError, BRVariable
|
||||
|
||||
from orm.models import Build, LogMessage, Target
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import signal
|
||||
import os
|
||||
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
args = ""
|
||||
help = "Schedules and executes build requests as possible. "\
|
||||
"Does not return (interrupt with Ctrl-C)"
|
||||
|
||||
@transaction.atomic
|
||||
def _selectBuildEnvironment(self):
|
||||
bec = getBuildEnvironmentController(lock=BuildEnvironment.LOCK_FREE)
|
||||
bec.be.lock = BuildEnvironment.LOCK_LOCK
|
||||
bec.be.save()
|
||||
return bec
|
||||
|
||||
@transaction.atomic
|
||||
def _selectBuildRequest(self):
|
||||
br = BuildRequest.objects.filter(state=BuildRequest.REQ_QUEUED).first()
|
||||
return br
|
||||
|
||||
def schedule(self):
|
||||
try:
|
||||
# select the build environment and the request to build
|
||||
br = self._selectBuildRequest()
|
||||
if br:
|
||||
br.state = BuildRequest.REQ_INPROGRESS
|
||||
br.save()
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
bec = self._selectBuildEnvironment()
|
||||
except IndexError as e:
|
||||
# we could not find a BEC; postpone the BR
|
||||
br.state = BuildRequest.REQ_QUEUED
|
||||
br.save()
|
||||
logger.debug("runbuilds: No build env (%s)" % e)
|
||||
return
|
||||
|
||||
logger.info("runbuilds: starting build %s, environment %s" %
|
||||
(br, bec.be))
|
||||
|
||||
# let the build request know where it is being executed
|
||||
br.environment = bec.be
|
||||
br.save()
|
||||
|
||||
# this triggers an async build
|
||||
bec.triggerBuild(br.brbitbake, br.brlayer_set.all(),
|
||||
br.brvariable_set.all(), br.brtarget_set.all(),
|
||||
"%d:%d" % (br.pk, bec.be.pk))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("runbuilds: Error launching build %s" % e)
|
||||
traceback.print_exc()
|
||||
if "[Errno 111] Connection refused" in str(e):
|
||||
# Connection refused, read toaster_server.out
|
||||
errmsg = bec.readServerLogFile()
|
||||
else:
|
||||
errmsg = str(e)
|
||||
|
||||
BRError.objects.create(req=br, errtype=str(type(e)), errmsg=errmsg,
|
||||
traceback=traceback.format_exc())
|
||||
br.state = BuildRequest.REQ_FAILED
|
||||
br.save()
|
||||
bec.be.lock = BuildEnvironment.LOCK_FREE
|
||||
bec.be.save()
|
||||
# Cancel the pending build and report the exception to the UI
|
||||
log_object = LogMessage.objects.create(
|
||||
build = br.build,
|
||||
level = LogMessage.EXCEPTION,
|
||||
message = errmsg)
|
||||
log_object.save()
|
||||
br.build.outcome = Build.FAILED
|
||||
br.build.save()
|
||||
|
||||
def archive(self):
|
||||
for br in BuildRequest.objects.filter(state=BuildRequest.REQ_ARCHIVE):
|
||||
if br.build is None:
|
||||
br.state = BuildRequest.REQ_FAILED
|
||||
else:
|
||||
br.state = BuildRequest.REQ_COMPLETED
|
||||
br.save()
|
||||
|
||||
def cleanup(self):
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
# environments locked for more than 30 seconds
|
||||
# they should be unlocked
|
||||
BuildEnvironment.objects.filter(
|
||||
Q(buildrequest__state__in=[BuildRequest.REQ_FAILED,
|
||||
BuildRequest.REQ_COMPLETED,
|
||||
BuildRequest.REQ_CANCELLING]) &
|
||||
Q(lock=BuildEnvironment.LOCK_LOCK) &
|
||||
Q(updated__lt=timezone.now() - timedelta(seconds=30))
|
||||
).update(lock=BuildEnvironment.LOCK_FREE)
|
||||
|
||||
# update all Builds that were in progress and failed to start
|
||||
for br in BuildRequest.objects.filter(
|
||||
state=BuildRequest.REQ_FAILED,
|
||||
build__outcome=Build.IN_PROGRESS):
|
||||
# transpose the launch errors in ToasterExceptions
|
||||
br.build.outcome = Build.FAILED
|
||||
for brerror in br.brerror_set.all():
|
||||
logger.debug("Saving error %s" % brerror)
|
||||
LogMessage.objects.create(build=br.build,
|
||||
level=LogMessage.EXCEPTION,
|
||||
message=brerror.errmsg)
|
||||
br.build.save()
|
||||
|
||||
# we don't have a true build object here; hence, toasterui
|
||||
# didn't have a change to release the BE lock
|
||||
br.environment.lock = BuildEnvironment.LOCK_FREE
|
||||
br.environment.save()
|
||||
|
||||
# update all BuildRequests without a build created
|
||||
for br in BuildRequest.objects.filter(build=None):
|
||||
br.build = Build.objects.create(project=br.project,
|
||||
completed_on=br.updated,
|
||||
started_on=br.created)
|
||||
br.build.outcome = Build.FAILED
|
||||
try:
|
||||
br.build.machine = br.brvariable_set.get(name='MACHINE').value
|
||||
except BRVariable.DoesNotExist:
|
||||
pass
|
||||
br.save()
|
||||
# transpose target information
|
||||
for brtarget in br.brtarget_set.all():
|
||||
Target.objects.create(build=br.build,
|
||||
target=brtarget.target,
|
||||
task=brtarget.task)
|
||||
# transpose the launch errors in ToasterExceptions
|
||||
for brerror in br.brerror_set.all():
|
||||
LogMessage.objects.create(build=br.build,
|
||||
level=LogMessage.EXCEPTION,
|
||||
message=brerror.errmsg)
|
||||
|
||||
br.build.save()
|
||||
|
||||
# Make sure the LOCK is removed for builds which have been fully
|
||||
# cancelled
|
||||
for br in BuildRequest.objects.filter(
|
||||
Q(build__outcome=Build.CANCELLED) &
|
||||
Q(state=BuildRequest.REQ_CANCELLING) &
|
||||
~Q(environment=None)):
|
||||
br.environment.lock = BuildEnvironment.LOCK_FREE
|
||||
br.environment.save()
|
||||
|
||||
def runbuild(self):
|
||||
try:
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
logger.warning("runbuilds: cleanup exception %s" % str(e))
|
||||
|
||||
try:
|
||||
self.archive()
|
||||
except Exception as e:
|
||||
logger.warning("runbuilds: archive exception %s" % str(e))
|
||||
|
||||
try:
|
||||
self.schedule()
|
||||
except Exception as e:
|
||||
logger.warning("runbuilds: schedule exception %s" % str(e))
|
||||
|
||||
# Test to see if a build pre-maturely died due to a bitbake crash
|
||||
def check_dead_builds(self):
|
||||
do_cleanup = False
|
||||
try:
|
||||
for br in BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS):
|
||||
# Get the build directory
|
||||
if br.project.builddir:
|
||||
builddir = br.project.builddir
|
||||
else:
|
||||
builddir = '%s-toaster-%d' % (br.environment.builddir,br.project.id)
|
||||
# Check log to see if there is a recent traceback
|
||||
toaster_ui_log = os.path.join(builddir, 'toaster_ui.log')
|
||||
test_file = os.path.join(builddir, '._toaster_check.txt')
|
||||
os.system("tail -n 50 %s > %s" % (os.path.join(builddir, 'toaster_ui.log'),test_file))
|
||||
traceback_text = ''
|
||||
is_traceback = False
|
||||
with open(test_file,'r') as test_file_fd:
|
||||
test_file_tail = test_file_fd.readlines()
|
||||
for line in test_file_tail:
|
||||
if line.startswith('Traceback (most recent call last):'):
|
||||
traceback_text = line
|
||||
is_traceback = True
|
||||
elif line.startswith('NOTE: ToasterUI waiting for events'):
|
||||
# Ignore any traceback before new build start
|
||||
traceback_text = ''
|
||||
is_traceback = False
|
||||
elif line.startswith('Note: Toaster traceback auto-stop'):
|
||||
# Ignore any traceback before this previous traceback catch
|
||||
traceback_text = ''
|
||||
is_traceback = False
|
||||
elif is_traceback:
|
||||
traceback_text += line
|
||||
# Test the results
|
||||
is_stop = False
|
||||
if is_traceback:
|
||||
# Found a traceback
|
||||
errtype = 'Bitbake crash'
|
||||
errmsg = 'Bitbake crash\n' + traceback_text
|
||||
state = BuildRequest.REQ_FAILED
|
||||
# Clean up bitbake files
|
||||
bitbake_lock = os.path.join(builddir, 'bitbake.lock')
|
||||
if os.path.isfile(bitbake_lock):
|
||||
os.remove(bitbake_lock)
|
||||
bitbake_sock = os.path.join(builddir, 'bitbake.sock')
|
||||
if os.path.isfile(bitbake_sock):
|
||||
os.remove(bitbake_sock)
|
||||
if os.path.isfile(test_file):
|
||||
os.remove(test_file)
|
||||
# Add note to ignore this traceback on next check
|
||||
os.system('echo "Note: Toaster traceback auto-stop" >> %s' % toaster_ui_log)
|
||||
is_stop = True
|
||||
# Add more tests here
|
||||
#elif ...
|
||||
# Stop the build request?
|
||||
if is_stop:
|
||||
brerror = BRError(
|
||||
req = br,
|
||||
errtype = errtype,
|
||||
errmsg = errmsg,
|
||||
traceback = traceback_text,
|
||||
)
|
||||
brerror.save()
|
||||
br.state = state
|
||||
br.save()
|
||||
do_cleanup = True
|
||||
# Do cleanup
|
||||
if do_cleanup:
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
logger.error("runbuilds: Error in check_dead_builds %s" % e)
|
||||
|
||||
def handle(self, **options):
|
||||
pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."),
|
||||
".runbuilds.pid")
|
||||
|
||||
with open(pidfile_path, 'w') as pidfile:
|
||||
pidfile.write("%s" % os.getpid())
|
||||
|
||||
# Clean up any stale/failed builds from previous Toaster run
|
||||
self.runbuild()
|
||||
|
||||
signal.signal(signal.SIGUSR1, lambda sig, frame: None)
|
||||
|
||||
while True:
|
||||
sigset = signal.sigtimedwait([signal.SIGUSR1], 5)
|
||||
if sigset:
|
||||
for sig in sigset:
|
||||
# Consume each captured pending event
|
||||
self.runbuild()
|
||||
else:
|
||||
# Check for build exceptions
|
||||
self.check_dead_builds()
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BRBitbake',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('giturl', models.CharField(max_length=254)),
|
||||
('commit', models.CharField(max_length=254)),
|
||||
('dirpath', models.CharField(max_length=254)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BRError',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('errtype', models.CharField(max_length=100)),
|
||||
('errmsg', models.TextField()),
|
||||
('traceback', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BRLayer',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('giturl', models.CharField(max_length=254)),
|
||||
('commit', models.CharField(max_length=254)),
|
||||
('dirpath', models.CharField(max_length=254)),
|
||||
('layer_version', models.ForeignKey(to='orm.Layer_Version', null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BRTarget',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('target', models.CharField(max_length=100)),
|
||||
('task', models.CharField(max_length=100, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BRVariable',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('value', models.TextField(blank=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildEnvironment',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('address', models.CharField(max_length=254)),
|
||||
('betype', models.IntegerField(choices=[(0, b'local'), (1, b'ssh')])),
|
||||
('bbaddress', models.CharField(max_length=254, blank=True)),
|
||||
('bbport', models.IntegerField(default=-1)),
|
||||
('bbtoken', models.CharField(max_length=126, blank=True)),
|
||||
('bbstate', models.IntegerField(default=0, choices=[(0, b'stopped'), (1, b'started')])),
|
||||
('sourcedir', models.CharField(max_length=512, blank=True)),
|
||||
('builddir', models.CharField(max_length=512, blank=True)),
|
||||
('lock', models.IntegerField(default=0, choices=[(0, b'free'), (1, b'lock'), (2, b'running')])),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('state', models.IntegerField(default=0, choices=[(0, b'created'), (1, b'queued'), (2, b'in progress'), (3, b'completed'), (4, b'failed'), (5, b'deleted'), (6, b'archive')])),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('build', models.OneToOneField(null=True, to='orm.Build', on_delete=models.CASCADE)),
|
||||
('environment', models.ForeignKey(to='bldcontrol.BuildEnvironment', null=True, on_delete=models.CASCADE)),
|
||||
('project', models.ForeignKey(to='orm.Project', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='brvariable',
|
||||
name='req',
|
||||
field=models.ForeignKey(to='bldcontrol.BuildRequest', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='brtarget',
|
||||
name='req',
|
||||
field=models.ForeignKey(to='bldcontrol.BuildRequest', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='brlayer',
|
||||
name='req',
|
||||
field=models.ForeignKey(to='bldcontrol.BuildRequest', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='brerror',
|
||||
name='req',
|
||||
field=models.ForeignKey(to='bldcontrol.BuildRequest', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='brbitbake',
|
||||
name='req',
|
||||
field=models.OneToOneField(to='bldcontrol.BuildRequest', on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='buildenvironment',
|
||||
name='betype',
|
||||
field=models.IntegerField(choices=[(0, b'local')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0002_auto_20160120_1250'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='buildrequest',
|
||||
name='state',
|
||||
field=models.IntegerField(default=0, choices=[(0, b'created'), (1, b'queued'), (2, b'in progress'), (3, b'completed'), (4, b'failed'), (5, b'deleted'), (6, b'cancelling'), (7, b'archive')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0003_add_cancelling_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='buildenvironment',
|
||||
name='bbstate',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'stopped'), (1, 'started')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildenvironment',
|
||||
name='betype',
|
||||
field=models.IntegerField(choices=[(0, 'local')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildenvironment',
|
||||
name='lock',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'free'), (1, 'lock'), (2, 'running')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildrequest',
|
||||
name='state',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'created'), (1, 'queued'), (2, 'in progress'), (3, 'completed'), (4, 'failed'), (5, 'deleted'), (6, 'cancelling'), (7, 'archive')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0004_auto_20160523_1446'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='buildrequest',
|
||||
name='state',
|
||||
field=models.IntegerField(choices=[(0, 'created'), (1, 'queued'), (2, 'in progress'), (3, 'failed'), (4, 'deleted'), (5, 'cancelling'), (6, 'completed'), (7, 'archive')], default=0),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0005_reorder_buildrequest_states'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='brlayer',
|
||||
name='local_source_dir',
|
||||
field=models.CharField(max_length=254, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0006_brlayer_local_source_dir'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='brlayer',
|
||||
name='commit',
|
||||
field=models.CharField(max_length=254, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brlayer',
|
||||
name='dirpath',
|
||||
field=models.CharField(max_length=254, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brlayer',
|
||||
name='giturl',
|
||||
field=models.CharField(max_length=254, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-06 03:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bldcontrol', '0007_brlayers_optional_gitinfo'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='brbitbake',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brerror',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brlayer',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brtarget',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='brvariable',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildenvironment',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='buildrequest',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
165
sources/poky/bitbake/lib/toaster/bldcontrol/models.py
Normal file
165
sources/poky/bitbake/lib/toaster/bldcontrol/models.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_str
|
||||
from orm.models import Project, Build, Layer_Version
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("toaster")
|
||||
# a BuildEnvironment is the equivalent of the "build/" directory on the localhost
|
||||
class BuildEnvironment(models.Model):
|
||||
SERVER_STOPPED = 0
|
||||
SERVER_STARTED = 1
|
||||
SERVER_STATE = (
|
||||
(SERVER_STOPPED, "stopped"),
|
||||
(SERVER_STARTED, "started"),
|
||||
)
|
||||
|
||||
TYPE_LOCAL = 0
|
||||
TYPE = (
|
||||
(TYPE_LOCAL, "local"),
|
||||
)
|
||||
|
||||
LOCK_FREE = 0
|
||||
LOCK_LOCK = 1
|
||||
LOCK_RUNNING = 2
|
||||
LOCK_STATE = (
|
||||
(LOCK_FREE, "free"),
|
||||
(LOCK_LOCK, "lock"),
|
||||
(LOCK_RUNNING, "running"),
|
||||
)
|
||||
|
||||
address = models.CharField(max_length = 254)
|
||||
betype = models.IntegerField(choices = TYPE)
|
||||
bbaddress = models.CharField(max_length = 254, blank = True)
|
||||
bbport = models.IntegerField(default = -1)
|
||||
bbtoken = models.CharField(max_length = 126, blank = True)
|
||||
bbstate = models.IntegerField(choices = SERVER_STATE, default = SERVER_STOPPED)
|
||||
sourcedir = models.CharField(max_length = 512, blank = True)
|
||||
builddir = models.CharField(max_length = 512, blank = True)
|
||||
lock = models.IntegerField(choices = LOCK_STATE, default = LOCK_FREE)
|
||||
created = models.DateTimeField(auto_now_add = True)
|
||||
updated = models.DateTimeField(auto_now = True)
|
||||
|
||||
def get_artifact(self, path):
|
||||
if self.betype == BuildEnvironment.TYPE_LOCAL:
|
||||
return open(path, "r")
|
||||
raise NotImplementedError("FIXME: artifact download not implemented "\
|
||||
"for build environment type %s" % \
|
||||
self.get_betype_display())
|
||||
|
||||
def has_artifact(self, path):
|
||||
import os
|
||||
if self.betype == BuildEnvironment.TYPE_LOCAL:
|
||||
return os.path.exists(path)
|
||||
raise NotImplementedError("FIXME: has artifact not implemented for "\
|
||||
"build environment type %s" % \
|
||||
self.get_betype_display())
|
||||
|
||||
# a BuildRequest is a request that the scheduler will build using a BuildEnvironment
|
||||
# the build request queue is the table itself, ordered by state
|
||||
|
||||
class BuildRequest(models.Model):
|
||||
REQ_CREATED = 0
|
||||
REQ_QUEUED = 1
|
||||
REQ_INPROGRESS = 2
|
||||
REQ_FAILED = 3
|
||||
REQ_DELETED = 4
|
||||
REQ_CANCELLING = 5
|
||||
REQ_COMPLETED = 6
|
||||
REQ_ARCHIVE = 7
|
||||
|
||||
REQUEST_STATE = (
|
||||
(REQ_CREATED, "created"),
|
||||
(REQ_QUEUED, "queued"),
|
||||
(REQ_INPROGRESS, "in progress"),
|
||||
(REQ_FAILED, "failed"),
|
||||
(REQ_DELETED, "deleted"),
|
||||
(REQ_CANCELLING, "cancelling"),
|
||||
(REQ_COMPLETED, "completed"),
|
||||
(REQ_ARCHIVE, "archive"),
|
||||
)
|
||||
|
||||
search_allowed_fields = ("brtarget__target", "build__project__name")
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE)
|
||||
build = models.OneToOneField(Build, on_delete=models.CASCADE, null = True) # TODO: toasterui should set this when Build is created
|
||||
environment = models.ForeignKey(BuildEnvironment, on_delete=models.CASCADE, null = True)
|
||||
state = models.IntegerField(choices = REQUEST_STATE, default = REQ_CREATED)
|
||||
created = models.DateTimeField(auto_now_add = True)
|
||||
updated = models.DateTimeField(auto_now = True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BuildRequest, self).__init__(*args, **kwargs)
|
||||
# Save the old state in case it's about to be modified
|
||||
self.old_state = self.state
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Check that the state we're trying to set is not going backwards
|
||||
# e.g. from REQ_FAILED to REQ_INPROGRESS
|
||||
if self.old_state != self.state and self.old_state > self.state:
|
||||
logger.warning("Invalid state change requested: "
|
||||
"Cannot go from %s to %s - ignoring request" %
|
||||
(BuildRequest.REQUEST_STATE[self.old_state][1],
|
||||
BuildRequest.REQUEST_STATE[self.state][1])
|
||||
)
|
||||
# Set property back to the old value
|
||||
self.state = self.old_state
|
||||
return
|
||||
|
||||
super(BuildRequest, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
def get_duration(self):
|
||||
return (self.updated - self.created).total_seconds()
|
||||
|
||||
def get_sorted_target_list(self):
|
||||
tgts = self.brtarget_set.order_by( 'target' );
|
||||
return( tgts );
|
||||
|
||||
def get_machine(self):
|
||||
return self.brvariable_set.get(name="MACHINE").value
|
||||
|
||||
def __str__(self):
|
||||
return force_str('%s %s' % (self.project, self.get_state_display()))
|
||||
|
||||
# These tables specify the settings for running an actual build.
|
||||
# They MUST be kept in sync with the tables in orm.models.Project*
|
||||
|
||||
|
||||
class BRLayer(models.Model):
|
||||
req = models.ForeignKey(BuildRequest, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
giturl = models.CharField(max_length=254, null=True)
|
||||
local_source_dir = models.CharField(max_length=254, null=True)
|
||||
commit = models.CharField(max_length=254, null=True)
|
||||
dirpath = models.CharField(max_length=254, null=True)
|
||||
layer_version = models.ForeignKey(Layer_Version, on_delete=models.CASCADE, null=True)
|
||||
|
||||
class BRBitbake(models.Model):
|
||||
req = models.OneToOneField(BuildRequest, on_delete=models.CASCADE) # only one bitbake for a request
|
||||
giturl = models.CharField(max_length =254)
|
||||
commit = models.CharField(max_length = 254)
|
||||
dirpath = models.CharField(max_length = 254)
|
||||
|
||||
class BRVariable(models.Model):
|
||||
req = models.ForeignKey(BuildRequest, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
value = models.TextField(blank = True)
|
||||
|
||||
class BRTarget(models.Model):
|
||||
req = models.ForeignKey(BuildRequest, on_delete=models.CASCADE)
|
||||
target = models.CharField(max_length=100)
|
||||
task = models.CharField(max_length=100, null=True)
|
||||
|
||||
class BRError(models.Model):
|
||||
req = models.ForeignKey(BuildRequest, on_delete=models.CASCADE)
|
||||
errtype = models.CharField(max_length=100)
|
||||
errmsg = models.TextField()
|
||||
traceback = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%s)" % (self.errmsg, self.req)
|
||||
5
sources/poky/bitbake/lib/toaster/bldcontrol/views.py
Normal file
5
sources/poky/bitbake/lib/toaster/bldcontrol/views.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
# Create your views here.
|
||||
1
sources/poky/bitbake/lib/toaster/logs/.gitignore
vendored
Normal file
1
sources/poky/bitbake/lib/toaster/logs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log*
|
||||
16
sources/poky/bitbake/lib/toaster/manage.py
Executable file
16
sources/poky/bitbake/lib/toaster/manage.py
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright BitBake Contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "toastermain.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
0
sources/poky/bitbake/lib/toaster/orm/__init__.py
Normal file
0
sources/poky/bitbake/lib/toaster/orm/__init__.py
Normal file
30
sources/poky/bitbake/lib/toaster/orm/fixtures/README
Normal file
30
sources/poky/bitbake/lib/toaster/orm/fixtures/README
Normal file
@@ -0,0 +1,30 @@
|
||||
# Fixtures directory
|
||||
|
||||
Fixtures are data dumps that can be loaded into Toaster's database to provide
|
||||
configuration and data.
|
||||
|
||||
In this directory we have the fixtures which are loaded the first time you start Toaster.
|
||||
This is to provide useful default values and metadata to Toaster.
|
||||
|
||||
- settings.xml This Contains Toaster wide settings, such as the default values for
|
||||
certain bitbake variables.
|
||||
|
||||
- poky.xml This is the default release data for supported poky based setup
|
||||
|
||||
- oe-core.xml This is the default release data for supported oe-core based setups
|
||||
|
||||
# Custom data/configuration
|
||||
|
||||
- custom.xml
|
||||
|
||||
To add custom initial data/configuration to Toaster place a file called
|
||||
"custom.xml" in this directory. If present it will be loaded into the database.
|
||||
We suggest that this is used to overlay any configuration already done.
|
||||
All objects loaded with the same primary keys overwrite the existing data.
|
||||
Data can be provided in XML, JSON and if installed YAML formats.
|
||||
|
||||
# To load data at any point in time
|
||||
|
||||
Use the django management command manage.py loaddata <your fixture file>
|
||||
For further information see the Django command documentation at:
|
||||
https://docs.djangoproject.com/en/3.2/ref/django-admin/#django-admin-loaddata
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2017 Intel Corp.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
# published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
# This is sample software. Rename it to 'custom_toaster_append.sh' and
|
||||
# enable the respective custom sections.
|
||||
|
||||
verbose=0
|
||||
if [ $verbose -ne 0 ] ; then
|
||||
echo "custom_toaster_append.sh:$*"
|
||||
fi
|
||||
|
||||
if [ "toaster_prepend" = "$1" ] ; then
|
||||
echo "Add custom actions here when Toaster script is started"
|
||||
fi
|
||||
|
||||
if [ "web_start_postpend" = "$1" ] ; then
|
||||
echo "Add custom actions here after Toaster web service is started"
|
||||
fi
|
||||
|
||||
if [ "web_stop_postpend" = "$1" ] ; then
|
||||
echo "Add custom actions here after Toaster web service is stopped"
|
||||
fi
|
||||
|
||||
if [ "noweb_start_postpend" = "$1" ] ; then
|
||||
echo "Add custom actions here after Toaster (no web) service is started"
|
||||
fi
|
||||
|
||||
if [ "noweb_stop_postpend" = "$1" ] ; then
|
||||
echo "Add custom actions here after Toaster (no web) service is stopped"
|
||||
fi
|
||||
|
||||
if [ "toaster_postpend" = "$1" ] ; then
|
||||
echo "Add custom actions here after Toaster script is done"
|
||||
fi
|
||||
|
||||
447
sources/poky/bitbake/lib/toaster/orm/fixtures/gen_fixtures.py
Executable file
447
sources/poky/bitbake/lib/toaster/orm/fixtures/gen_fixtures.py
Executable file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
# ex:ts=4:sw=4:sts=4:et
|
||||
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
||||
#
|
||||
# Generate Toaster Fixtures for 'poky.xml' and 'oe-core.xml'
|
||||
#
|
||||
# Copyright (C) 2022 Wind River Systems
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Edit the 'current_releases' table for each new release cycle
|
||||
#
|
||||
# Usage: ./get_fixtures all
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
verbose = False
|
||||
|
||||
####################################
|
||||
# Releases
|
||||
#
|
||||
# https://wiki.yoctoproject.org/wiki/Releases
|
||||
#
|
||||
# NOTE: for the current releases table, it helps to keep continuing releases
|
||||
# in the same table positions since this minimizes the patch diff for review.
|
||||
# The order of the table does not matter since Toaster presents them sorted.
|
||||
#
|
||||
# Traditionally, the two most current releases are included in addition to the
|
||||
# 'master' branch and the local installation's 'HEAD'.
|
||||
# It is also policy to include all active LTS releases.
|
||||
#
|
||||
|
||||
# [Codename, Yocto Project Version, Release Date, Current Version, Support Level, Poky Version, BitBake branch]
|
||||
current_releases = [
|
||||
# Release slot #1
|
||||
['Kirkstone','4.0','April 2022','4.0.8 (March 2023)','Stable - Long Term Support (until Apr. 2024)','','2.0'],
|
||||
# Release slot #2 'local'
|
||||
['HEAD','HEAD','','Local Yocto Project','HEAD','','HEAD'],
|
||||
# Release slot #3 'master'
|
||||
['Master','master','','Yocto Project master','master','','master'],
|
||||
# Release slot #4
|
||||
['Mickledore','4.2','April 2023','4.2.0 (April 2023)','Support for 7 months (until October 2023)','','2.4'],
|
||||
# ['Langdale','4.1','October 2022','4.1.2 (January 2023)','Support for 7 months (until May 2023)','','2.2'],
|
||||
# ['Honister','3.4','October 2021','3.4.2 (February 2022)','Support for 7 months (until May 2022)','26.0','1.52'],
|
||||
# ['Hardknott','3.3','April 2021','3.3.5 (March 2022)','Stable - Support for 13 months (until Apr. 2022)','25.0','1.50'],
|
||||
# ['Gatesgarth','3.2','Oct 2020','3.2.4 (May 2021)','EOL','24.0','1.48'],
|
||||
# Optional Release slot #5
|
||||
['Dunfell','3.1','April 2020','3.1.23 (February 2023)','Stable - Long Term Support (until Apr. 2024)','23.0','1.46'],
|
||||
]
|
||||
|
||||
default_poky_layers = [
|
||||
'openembedded-core',
|
||||
'meta-poky',
|
||||
'meta-yocto-bsp',
|
||||
]
|
||||
|
||||
default_oe_core_layers = [
|
||||
'openembedded-core',
|
||||
]
|
||||
|
||||
####################################
|
||||
# Templates
|
||||
|
||||
prolog_template = '''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<!-- Set the project default value for DISTRO -->
|
||||
<object model="orm.toastersetting" pk="1">
|
||||
<field type="CharField" name="name">DEFCONF_DISTRO</field>
|
||||
<field type="CharField" name="value">{{distro}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
#<!-- Bitbake versions which correspond to the metadata release -->')
|
||||
bitbakeversion_poky_template = '''\
|
||||
<object model="orm.bitbakeversion" pk="{{bitbake_id}}">
|
||||
<field type="CharField" name="name">{{name}}</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">{{branch}}</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
'''
|
||||
bitbakeversion_oecore_template = '''\
|
||||
<object model="orm.bitbakeversion" pk="{{bitbake_id}}">
|
||||
<field type="CharField" name="name">{{name}}</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">{{bitbakeversion}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
# <!-- Releases available -->
|
||||
releases_available_template = '''\
|
||||
<object model="orm.release" pk="{{ra_count}}">
|
||||
<field type="CharField" name="name">{{name}}</field>
|
||||
<field type="CharField" name="description">{{description}}</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">{{ra_count}}</field>
|
||||
<field type="CharField" name="branch_name">{{release}}</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds {{help_source}}.</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
# <!-- Default project layers for each release -->
|
||||
default_layers_template = '''\
|
||||
<object model="orm.releasedefaultlayer" pk="{{rdl_count}}">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">{{release_id}}</field>
|
||||
<field type="CharField" name="layer_name">{{layer}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
default_layers_preface = '''\
|
||||
<!-- Default layers provided by poky
|
||||
openembedded-core
|
||||
meta-poky
|
||||
meta-yocto-bsp
|
||||
-->
|
||||
'''
|
||||
|
||||
layer_poky_template = '''\
|
||||
<object model="orm.layer" pk="{{layer_id}}">
|
||||
<field type="CharField" name="name">{{layer}}</field>
|
||||
<field type="CharField" name="layer_index_url"></field>
|
||||
<field type="CharField" name="vcs_url">{{vcs_url}}</field>
|
||||
<field type="CharField" name="vcs_web_url">{{vcs_web_url}}</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">{{vcs_web_tree_base_url}}</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">{{vcs_web_file_base_url}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
layer_oe_core_template = '''\
|
||||
<object model="orm.layer" pk="{{layer_id}}">
|
||||
<field type="CharField" name="name">{{layer}}</field>
|
||||
<field type="CharField" name="vcs_url">{{vcs_url}}</field>
|
||||
<field type="CharField" name="vcs_web_url">{{vcs_web_url}}</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">{{vcs_web_tree_base_url}}</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">{{vcs_web_file_base_url}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
layer_version_template = '''\
|
||||
<object model="orm.layer_version" pk="{{lv_count}}">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">{{layer_id}}</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">{{release_id}}</field>
|
||||
<field type="CharField" name="branch">{{branch}}</field>
|
||||
<field type="CharField" name="dirpath">{{dirpath}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
layer_version_HEAD_template = '''\
|
||||
<object model="orm.layer_version" pk="{{lv_count}}">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">{{layer_id}}</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">{{release_id}}</field>
|
||||
<field type="CharField" name="branch">{{branch}}</field>
|
||||
<field type="CharField" name="commit">{{commit}}</field>
|
||||
<field type="CharField" name="dirpath">{{dirpath}}</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
layer_version_oe_core_template = '''\
|
||||
<object model="orm.layer_version" pk="1">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="local_path">OE-CORE-LAYER-DIR</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
</object>
|
||||
'''
|
||||
|
||||
epilog_template = '''\
|
||||
</django-objects>
|
||||
'''
|
||||
|
||||
#################################
|
||||
# Helper Routines
|
||||
#
|
||||
|
||||
def print_str(str,fd):
|
||||
# Avoid extra newline at end
|
||||
if str and (str[-1] == '\n'):
|
||||
str = str[0:-1]
|
||||
print(str,file=fd)
|
||||
|
||||
def print_template(template,params,fd):
|
||||
for line in template.split('\n'):
|
||||
p = line.find('{{')
|
||||
while p > 0:
|
||||
q = line.find('}}')
|
||||
key = line[p+2:q]
|
||||
if key in params:
|
||||
line = line[0:p] + params[key] + line[q+2:]
|
||||
else:
|
||||
line = line[0:p] + '?' + key + '?' + line[q+2:]
|
||||
p = line.find('{{')
|
||||
if line:
|
||||
print(line,file=fd)
|
||||
|
||||
#################################
|
||||
# Generate poky.xml
|
||||
#
|
||||
|
||||
def generate_poky():
|
||||
fd = open('poky.xml','w')
|
||||
|
||||
params = {}
|
||||
params['distro'] = 'poky'
|
||||
print_template(prolog_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(' <!-- Bitbake versions which correspond to the metadata release -->',fd)
|
||||
for i,release in enumerate(current_releases):
|
||||
params = {}
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('HEAD')): # 'master',
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['name'] = params['release']
|
||||
params['bitbake_id'] = str(i+1)
|
||||
params['branch'] = params['release']
|
||||
print_template(bitbakeversion_poky_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str('',fd)
|
||||
print_str(' <!-- Releases available -->',fd)
|
||||
for i,release in enumerate(current_releases):
|
||||
params = {}
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('HEAD')): #'master',
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['h_release'] = '?h={{release}}'
|
||||
params['name'] = params['release']
|
||||
params['ra_count'] = str(i+1)
|
||||
params['branch'] = params['release']
|
||||
|
||||
if 'HEAD' == params['release']:
|
||||
params['help_source'] = 'with the version of the Yocto Project you have cloned or downloaded to your computer'
|
||||
params['description'] = 'Local Yocto Project'
|
||||
params['name'] = 'local'
|
||||
else:
|
||||
params['help_source'] = 'using the tip of the <a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/{{h_release}}">Yocto Project {{Release}} branch</a>'
|
||||
params['description'] = 'Yocto Project {{release_version}} "{{Release}}"'
|
||||
if 'master' == params['release']:
|
||||
params['h_release'] = ''
|
||||
params['description'] = 'Yocto Project master'
|
||||
|
||||
print_template(releases_available_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(' <!-- Default project layers for each release -->',fd)
|
||||
rdl_count = 1
|
||||
for i,release in enumerate(current_releases):
|
||||
for j,layer in enumerate(default_poky_layers):
|
||||
params = {}
|
||||
params['layer'] = layer
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('master','HEAD')):
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['release_id'] = str(i+1)
|
||||
params['rdl_count'] = str(rdl_count)
|
||||
params['branch'] = params['release']
|
||||
print_template(default_layers_template,params,fd)
|
||||
rdl_count += 1
|
||||
print_str('',fd)
|
||||
|
||||
print_str(default_layers_preface,fd)
|
||||
lv_count = 1
|
||||
for i,layer in enumerate(default_poky_layers):
|
||||
params = {}
|
||||
params['layer'] = layer
|
||||
params['layer_id'] = str(i+1)
|
||||
params['vcs_url'] = 'git://git.yoctoproject.org/poky'
|
||||
params['vcs_web_url'] = 'https://git.yoctoproject.org/cgit/cgit.cgi/poky'
|
||||
params['vcs_web_tree_base_url'] = 'https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%'
|
||||
params['vcs_web_file_base_url'] = 'https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%'
|
||||
|
||||
if i:
|
||||
print_str('',fd)
|
||||
print_template(layer_poky_template,params,fd)
|
||||
for j,release in enumerate(current_releases):
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('master','HEAD')):
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['release_id'] = str(j+1)
|
||||
params['lv_count'] = str(lv_count)
|
||||
params['branch'] = params['release']
|
||||
params['commit'] = params['release']
|
||||
|
||||
params['dirpath'] = params['layer']
|
||||
if params['layer'] in ('openembedded-core'): #'openembedded-core',
|
||||
params['dirpath'] = 'meta'
|
||||
|
||||
if 'HEAD' == params['release']:
|
||||
print_template(layer_version_HEAD_template,params,fd)
|
||||
else:
|
||||
print_template(layer_version_template,params,fd)
|
||||
lv_count += 1
|
||||
|
||||
print_str(epilog_template,fd)
|
||||
fd.close()
|
||||
|
||||
#################################
|
||||
# Generate oe-core.xml
|
||||
#
|
||||
|
||||
def generate_oe_core():
|
||||
fd = open('oe-core.xml','w')
|
||||
|
||||
params = {}
|
||||
params['distro'] = 'nodistro'
|
||||
print_template(prolog_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(' <!-- Bitbake versions which correspond to the metadata release -->',fd)
|
||||
for i,release in enumerate(current_releases):
|
||||
params = {}
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['bitbakeversion'] = release[6]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('HEAD')): # 'master',
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['name'] = params['release']
|
||||
params['bitbake_id'] = str(i+1)
|
||||
params['branch'] = params['release']
|
||||
print_template(bitbakeversion_oecore_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(' <!-- Releases available -->',fd)
|
||||
for i,release in enumerate(current_releases):
|
||||
params = {}
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('HEAD')): #'master',
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['h_release'] = '?h={{release}}'
|
||||
params['name'] = params['release']
|
||||
params['ra_count'] = str(i+1)
|
||||
params['branch'] = params['release']
|
||||
|
||||
if 'HEAD' == params['release']:
|
||||
params['help_source'] = 'with the version of OpenEmbedded that you have cloned or downloaded to your computer'
|
||||
params['description'] = 'Local Openembedded'
|
||||
params['name'] = 'local'
|
||||
else:
|
||||
params['help_source'] = 'using the tip of the <a href=\\"https://cgit.openembedded.org/openembedded-core/log/{{h_release}}\\">OpenEmbedded {{Release}}</a> branch'
|
||||
params['description'] = 'Openembedded {{Release}}'
|
||||
if 'master' == params['release']:
|
||||
params['h_release'] = ''
|
||||
params['description'] = 'OpenEmbedded core master'
|
||||
params['Release'] = params['release']
|
||||
|
||||
print_template(releases_available_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(' <!-- Default layers for each release -->',fd)
|
||||
rdl_count = 1
|
||||
for i,release in enumerate(current_releases):
|
||||
for j,layer in enumerate(default_oe_core_layers):
|
||||
params = {}
|
||||
params['layer'] = layer
|
||||
params['release'] = release[0]
|
||||
params['Release'] = release[0]
|
||||
params['release_version'] = release[1]
|
||||
if not (params['release'] in ('master','HEAD')):
|
||||
params['release'] = params['release'][0].lower() + params['release'][1:]
|
||||
params['release_id'] = str(i+1)
|
||||
params['rdl_count'] = str(rdl_count)
|
||||
params['branch'] = params['release']
|
||||
print_template(default_layers_template,params,fd)
|
||||
rdl_count += 1
|
||||
print_str('',fd)
|
||||
|
||||
print_str('',fd)
|
||||
print_str(' <!-- Layer for the Local release -->',fd)
|
||||
lv_count = 1
|
||||
for i,layer in enumerate(default_oe_core_layers):
|
||||
params = {}
|
||||
params['layer'] = layer
|
||||
params['layer_id'] = str(i+1)
|
||||
params['vcs_url'] = 'git://git.openembedded.org/openembedded-core'
|
||||
params['vcs_web_url'] = 'https://cgit.openembedded.org/openembedded-core'
|
||||
params['vcs_web_tree_base_url'] = 'https://cgit.openembedded.org/openembedded-core/tree/%path%?h=%branch%'
|
||||
params['vcs_web_file_base_url'] = 'https://cgit.openembedded.org/openembedded-core/tree/%path%?h=%branch%'
|
||||
if i:
|
||||
print_str('',fd)
|
||||
print_template(layer_oe_core_template,params,fd)
|
||||
|
||||
print_template(layer_version_oe_core_template,params,fd)
|
||||
print_str('',fd)
|
||||
|
||||
print_str(epilog_template,fd)
|
||||
fd.close()
|
||||
|
||||
#################################
|
||||
# Help
|
||||
#
|
||||
|
||||
def list_releases():
|
||||
print("Release ReleaseVer BitbakeVer Support Level")
|
||||
print("========== =========== ========== ==============================================")
|
||||
for release in current_releases:
|
||||
print("%10s %10s %11s %s" % (release[0],release[1],release[6],release[4]))
|
||||
|
||||
#################################
|
||||
# main
|
||||
#
|
||||
|
||||
def main(argv):
|
||||
global verbose
|
||||
|
||||
parser = argparse.ArgumentParser(description='gen_fixtures.py: table generate the fixture files')
|
||||
parser.add_argument('--poky', '-p', action='store_const', const='poky', dest='command', help='Generate the poky.xml file')
|
||||
parser.add_argument('--oe-core', '-o', action='store_const', const='oe_core', dest='command', help='Generate the oe-core.xml file')
|
||||
parser.add_argument('--all', '-a', action='store_const', const='all', dest='command', help='Generate all fixture files')
|
||||
parser.add_argument('--list', '-l', action='store_const', const='list', dest='command', help='List the release table')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', dest='verbose', help='Enable verbose debugging output')
|
||||
args = parser.parse_args()
|
||||
|
||||
verbose = args.verbose
|
||||
if 'poky' == args.command:
|
||||
generate_poky()
|
||||
elif 'oe_core' == args.command:
|
||||
generate_oe_core()
|
||||
elif 'all' == args.command:
|
||||
generate_poky()
|
||||
generate_oe_core()
|
||||
elif 'all' == args.command:
|
||||
list_releases()
|
||||
elif 'list' == args.command:
|
||||
list_releases()
|
||||
|
||||
else:
|
||||
print("No command for 'gen_fixtures.py' selected")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
113
sources/poky/bitbake/lib/toaster/orm/fixtures/oe-core.xml
Normal file
113
sources/poky/bitbake/lib/toaster/orm/fixtures/oe-core.xml
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<!-- Set the project default value for DISTRO -->
|
||||
<object model="orm.toastersetting" pk="1">
|
||||
<field type="CharField" name="name">DEFCONF_DISTRO</field>
|
||||
<field type="CharField" name="value">nodistro</field>
|
||||
</object>
|
||||
|
||||
<!-- Bitbake versions which correspond to the metadata release -->
|
||||
<object model="orm.bitbakeversion" pk="1">
|
||||
<field type="CharField" name="name">kirkstone</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">2.0</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="2">
|
||||
<field type="CharField" name="name">HEAD</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="3">
|
||||
<field type="CharField" name="name">master</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">master</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="4">
|
||||
<field type="CharField" name="name">mickledore</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">2.4</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="5">
|
||||
<field type="CharField" name="name">dunfell</field>
|
||||
<field type="CharField" name="giturl">git://git.openembedded.org/bitbake</field>
|
||||
<field type="CharField" name="branch">1.46</field>
|
||||
</object>
|
||||
|
||||
<!-- Releases available -->
|
||||
<object model="orm.release" pk="1">
|
||||
<field type="CharField" name="name">kirkstone</field>
|
||||
<field type="CharField" name="description">Openembedded Kirkstone</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">1</field>
|
||||
<field type="CharField" name="branch_name">kirkstone</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=kirkstone\">OpenEmbedded Kirkstone</a> branch.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="2">
|
||||
<field type="CharField" name="name">local</field>
|
||||
<field type="CharField" name="description">Local Openembedded</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">2</field>
|
||||
<field type="CharField" name="branch_name">HEAD</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds with the version of OpenEmbedded that you have cloned or downloaded to your computer.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="3">
|
||||
<field type="CharField" name="name">master</field>
|
||||
<field type="CharField" name="description">OpenEmbedded core master</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">3</field>
|
||||
<field type="CharField" name="branch_name">master</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href=\"https://cgit.openembedded.org/openembedded-core/log/\">OpenEmbedded master</a> branch.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="4">
|
||||
<field type="CharField" name="name">mickledore</field>
|
||||
<field type="CharField" name="description">Openembedded Mickledore</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">4</field>
|
||||
<field type="CharField" name="branch_name">mickledore</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=mickledore\">OpenEmbedded Mickledore</a> branch.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="5">
|
||||
<field type="CharField" name="name">dunfell</field>
|
||||
<field type="CharField" name="description">Openembedded Dunfell</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">5</field>
|
||||
<field type="CharField" name="branch_name">dunfell</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=dunfell\">OpenEmbedded Dunfell</a> branch.</field>
|
||||
</object>
|
||||
|
||||
<!-- Default layers for each release -->
|
||||
<object model="orm.releasedefaultlayer" pk="1">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="2">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="3">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="4">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="5">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
|
||||
|
||||
<!-- Layer for the Local release -->
|
||||
<object model="orm.layer" pk="1">
|
||||
<field type="CharField" name="name">openembedded-core</field>
|
||||
<field type="CharField" name="vcs_url">git://git.openembedded.org/openembedded-core</field>
|
||||
<field type="CharField" name="vcs_web_url">https://cgit.openembedded.org/openembedded-core</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">https://cgit.openembedded.org/openembedded-core/tree/%path%?h=%branch%</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">https://cgit.openembedded.org/openembedded-core/tree/%path%?h=%branch%</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="1">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="local_path">OE-CORE-LAYER-DIR</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
</object>
|
||||
|
||||
</django-objects>
|
||||
280
sources/poky/bitbake/lib/toaster/orm/fixtures/poky.xml
Normal file
280
sources/poky/bitbake/lib/toaster/orm/fixtures/poky.xml
Normal file
@@ -0,0 +1,280 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<!-- Set the project default value for DISTRO -->
|
||||
<object model="orm.toastersetting" pk="1">
|
||||
<field type="CharField" name="name">DEFCONF_DISTRO</field>
|
||||
<field type="CharField" name="value">poky</field>
|
||||
</object>
|
||||
|
||||
<!-- Bitbake versions which correspond to the metadata release -->
|
||||
<object model="orm.bitbakeversion" pk="1">
|
||||
<field type="CharField" name="name">kirkstone</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">kirkstone</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="2">
|
||||
<field type="CharField" name="name">HEAD</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="3">
|
||||
<field type="CharField" name="name">master</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">master</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="4">
|
||||
<field type="CharField" name="name">mickledore</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">mickledore</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
<object model="orm.bitbakeversion" pk="5">
|
||||
<field type="CharField" name="name">dunfell</field>
|
||||
<field type="CharField" name="giturl">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="branch">dunfell</field>
|
||||
<field type="CharField" name="dirpath">bitbake</field>
|
||||
</object>
|
||||
|
||||
|
||||
<!-- Releases available -->
|
||||
<object model="orm.release" pk="1">
|
||||
<field type="CharField" name="name">kirkstone</field>
|
||||
<field type="CharField" name="description">Yocto Project 4.0 "Kirkstone"</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">1</field>
|
||||
<field type="CharField" name="branch_name">kirkstone</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=kirkstone">Yocto Project Kirkstone branch</a>.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="2">
|
||||
<field type="CharField" name="name">local</field>
|
||||
<field type="CharField" name="description">Local Yocto Project</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">2</field>
|
||||
<field type="CharField" name="branch_name">HEAD</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds with the version of the Yocto Project you have cloned or downloaded to your computer.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="3">
|
||||
<field type="CharField" name="name">master</field>
|
||||
<field type="CharField" name="description">Yocto Project master</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">3</field>
|
||||
<field type="CharField" name="branch_name">master</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/">Yocto Project Master branch</a>.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="4">
|
||||
<field type="CharField" name="name">mickledore</field>
|
||||
<field type="CharField" name="description">Yocto Project 4.2 "Mickledore"</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">4</field>
|
||||
<field type="CharField" name="branch_name">mickledore</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=mickledore">Yocto Project Mickledore branch</a>.</field>
|
||||
</object>
|
||||
<object model="orm.release" pk="5">
|
||||
<field type="CharField" name="name">dunfell</field>
|
||||
<field type="CharField" name="description">Yocto Project 3.1 "Dunfell"</field>
|
||||
<field rel="ManyToOneRel" to="orm.bitbakeversion" name="bitbake_version">5</field>
|
||||
<field type="CharField" name="branch_name">dunfell</field>
|
||||
<field type="TextField" name="helptext">Toaster will run your builds using the tip of the <a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=dunfell">Yocto Project Dunfell branch</a>.</field>
|
||||
</object>
|
||||
|
||||
<!-- Default project layers for each release -->
|
||||
<object model="orm.releasedefaultlayer" pk="1">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="2">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="layer_name">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="3">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="layer_name">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="4">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="5">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="layer_name">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="6">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="layer_name">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="7">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="8">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="layer_name">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="9">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="layer_name">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="10">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="11">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="layer_name">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="12">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="layer_name">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="13">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="layer_name">openembedded-core</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="14">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="layer_name">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.releasedefaultlayer" pk="15">
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="layer_name">meta-yocto-bsp</field>
|
||||
</object>
|
||||
|
||||
<!-- Default layers provided by poky
|
||||
openembedded-core
|
||||
meta-poky
|
||||
meta-yocto-bsp
|
||||
-->
|
||||
<object model="orm.layer" pk="1">
|
||||
<field type="CharField" name="name">openembedded-core</field>
|
||||
<field type="CharField" name="layer_index_url"></field>
|
||||
<field type="CharField" name="vcs_url">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="vcs_web_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="1">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="branch">kirkstone</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="2">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="commit">HEAD</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="3">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="branch">master</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="4">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="branch">mickledore</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="5">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">1</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="branch">dunfell</field>
|
||||
<field type="CharField" name="dirpath">meta</field>
|
||||
</object>
|
||||
|
||||
<object model="orm.layer" pk="2">
|
||||
<field type="CharField" name="name">meta-poky</field>
|
||||
<field type="CharField" name="layer_index_url"></field>
|
||||
<field type="CharField" name="vcs_url">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="vcs_web_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="6">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">2</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="branch">kirkstone</field>
|
||||
<field type="CharField" name="dirpath">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="7">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">2</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="commit">HEAD</field>
|
||||
<field type="CharField" name="dirpath">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="8">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">2</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="branch">master</field>
|
||||
<field type="CharField" name="dirpath">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="9">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">2</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="branch">mickledore</field>
|
||||
<field type="CharField" name="dirpath">meta-poky</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="10">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">2</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="branch">dunfell</field>
|
||||
<field type="CharField" name="dirpath">meta-poky</field>
|
||||
</object>
|
||||
|
||||
<object model="orm.layer" pk="3">
|
||||
<field type="CharField" name="name">meta-yocto-bsp</field>
|
||||
<field type="CharField" name="layer_index_url"></field>
|
||||
<field type="CharField" name="vcs_url">git://git.yoctoproject.org/poky</field>
|
||||
<field type="CharField" name="vcs_web_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky</field>
|
||||
<field type="CharField" name="vcs_web_tree_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
<field type="CharField" name="vcs_web_file_base_url">https://git.yoctoproject.org/cgit/cgit.cgi/poky/tree/%path%?h=%branch%</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="11">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">3</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">1</field>
|
||||
<field type="CharField" name="branch">kirkstone</field>
|
||||
<field type="CharField" name="dirpath">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="12">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">3</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">2</field>
|
||||
<field type="CharField" name="branch">HEAD</field>
|
||||
<field type="CharField" name="commit">HEAD</field>
|
||||
<field type="CharField" name="dirpath">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="13">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">3</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">3</field>
|
||||
<field type="CharField" name="branch">master</field>
|
||||
<field type="CharField" name="dirpath">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="14">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">3</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">4</field>
|
||||
<field type="CharField" name="branch">mickledore</field>
|
||||
<field type="CharField" name="dirpath">meta-yocto-bsp</field>
|
||||
</object>
|
||||
<object model="orm.layer_version" pk="15">
|
||||
<field rel="ManyToOneRel" to="orm.layer" name="layer">3</field>
|
||||
<field type="IntegerField" name="layer_source">0</field>
|
||||
<field rel="ManyToOneRel" to="orm.release" name="release">5</field>
|
||||
<field type="CharField" name="branch">dunfell</field>
|
||||
<field type="CharField" name="dirpath">meta-yocto-bsp</field>
|
||||
</object>
|
||||
</django-objects>
|
||||
33
sources/poky/bitbake/lib/toaster/orm/fixtures/settings.xml
Normal file
33
sources/poky/bitbake/lib/toaster/orm/fixtures/settings.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<!-- Default project settings -->
|
||||
<!-- pk=1 is DISTRO -->
|
||||
<object model="orm.toastersetting" pk="2">
|
||||
<field type="CharField" name="name">DEFAULT_RELEASE</field>
|
||||
<field type="CharField" name="value">master</field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="3">
|
||||
<field type="CharField" name="name">DEFCONF_PACKAGE_CLASSES</field>
|
||||
<field type="CharField" name="value">package_rpm</field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="4">
|
||||
<field type="CharField" name="name">DEFCONF_MACHINE</field>
|
||||
<field type="CharField" name="value">qemux86-64</field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="5">
|
||||
<field type="CharField" name="name">DEFCONF_SSTATE_DIR</field>
|
||||
<field type="CharField" name="value">${TOPDIR}/../sstate-cache</field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="6">
|
||||
<field type="CharField" name="name">DEFCONF_IMAGE_INSTALL:append</field>
|
||||
<field type="CharField" name="value"></field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="7">
|
||||
<field type="CharField" name="name">DEFCONF_IMAGE_FSTYPES</field>
|
||||
<field type="CharField" name="value">ext3 jffs2 tar.bz2</field>
|
||||
</object>
|
||||
<object model="orm.toastersetting" pk="8">
|
||||
<field type="CharField" name="name">DEFCONF_DL_DIR</field>
|
||||
<field type="CharField" name="value">${TOPDIR}/../downloads</field>
|
||||
</object>
|
||||
</django-objects>
|
||||
@@ -0,0 +1,290 @@
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016-2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from orm.models import Layer, Release, Layer_Version
|
||||
from orm.models import LayerVersionDependency, Machine, Recipe
|
||||
from orm.models import Distro
|
||||
from orm.models import ToasterSetting
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
DEFAULT_LAYERINDEX_SERVER = "https://layers.openembedded.org/layerindex/api/"
|
||||
|
||||
# Add path to bitbake modules for layerindexlib
|
||||
# lib/toaster/orm/management/commands/lsupdates.py (abspath)
|
||||
# lib/toaster/orm/management/commands (dirname)
|
||||
# lib/toaster/orm/management (dirname)
|
||||
# lib/toaster/orm (dirname)
|
||||
# lib/toaster/ (dirname)
|
||||
# lib/ (dirname)
|
||||
path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
sys.path.insert(0, path)
|
||||
|
||||
import layerindexlib
|
||||
|
||||
|
||||
class Spinner(threading.Thread):
|
||||
""" A simple progress spinner to indicate download/parsing is happening"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Spinner, self).__init__(*args, **kwargs)
|
||||
self.daemon = True
|
||||
self.signal = True
|
||||
|
||||
def run(self):
|
||||
os.system('setterm -cursor off')
|
||||
while self.signal:
|
||||
for char in ["/", "-", "\\", "|"]:
|
||||
sys.stdout.write("\r" + char)
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.25)
|
||||
os.system('setterm -cursor on')
|
||||
|
||||
def stop(self):
|
||||
self.signal = False
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
args = ""
|
||||
help = "Updates locally cached information from a layerindex server"
|
||||
|
||||
def mini_progress(self, what, i, total):
|
||||
i = i + 1
|
||||
pec = (float(i)/float(total))*100
|
||||
|
||||
sys.stdout.write("\rUpdating %s %d%%" %
|
||||
(what,
|
||||
pec))
|
||||
sys.stdout.flush()
|
||||
if int(pec) == 100:
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Fetches layer, recipe and machine information from a layerindex
|
||||
server
|
||||
"""
|
||||
os.system('setterm -cursor off')
|
||||
|
||||
self.apiurl = DEFAULT_LAYERINDEX_SERVER
|
||||
if ToasterSetting.objects.filter(name='CUSTOM_LAYERINDEX_SERVER').count() == 1:
|
||||
self.apiurl = ToasterSetting.objects.get(name = 'CUSTOM_LAYERINDEX_SERVER').value
|
||||
|
||||
assert self.apiurl is not None
|
||||
|
||||
# update branches; only those that we already have names listed in the
|
||||
# Releases table
|
||||
allowed_branch_names = [rel.branch_name
|
||||
for rel in Release.objects.all()]
|
||||
if len(allowed_branch_names) == 0:
|
||||
raise Exception("Failed to make list of branches to fetch")
|
||||
|
||||
logger.info("Fetching metadata for %s",
|
||||
" ".join(allowed_branch_names))
|
||||
|
||||
# We require a non-empty bb.data, but we can fake it with a dictionary
|
||||
layerindex = layerindexlib.LayerIndex({"DUMMY" : "VALUE"})
|
||||
|
||||
http_progress = Spinner()
|
||||
http_progress.start()
|
||||
|
||||
if allowed_branch_names:
|
||||
url_branches = ";branch=%s" % ','.join(allowed_branch_names)
|
||||
else:
|
||||
url_branches = ""
|
||||
layerindex.load_layerindex("%s%s" % (self.apiurl, url_branches))
|
||||
|
||||
http_progress.stop()
|
||||
|
||||
# We know we're only processing one entry, so we reference it here
|
||||
# (this is cheating...)
|
||||
index = layerindex.indexes[0]
|
||||
|
||||
# Map the layer index branches to toaster releases
|
||||
li_branch_id_to_toaster_release = {}
|
||||
|
||||
logger.info("Processing releases")
|
||||
|
||||
total = len(index.branches)
|
||||
for i, id in enumerate(index.branches):
|
||||
li_branch_id_to_toaster_release[id] = \
|
||||
Release.objects.get(name=index.branches[id].name)
|
||||
self.mini_progress("Releases", i, total)
|
||||
|
||||
# keep a track of the layerindex (li) id mappings so that
|
||||
# layer_versions can be created for these layers later on
|
||||
li_layer_id_to_toaster_layer_id = {}
|
||||
|
||||
logger.info("Processing layers")
|
||||
|
||||
total = len(index.layerItems)
|
||||
for i, id in enumerate(index.layerItems):
|
||||
try:
|
||||
l, created = Layer.objects.get_or_create(name=index.layerItems[id].name)
|
||||
l.up_date = index.layerItems[id].updated
|
||||
l.summary = index.layerItems[id].summary
|
||||
l.description = index.layerItems[id].description
|
||||
|
||||
if created:
|
||||
# predefined layers in the fixtures (for example poky.xml)
|
||||
# always preempt the Layer Index for these values
|
||||
l.vcs_url = index.layerItems[id].vcs_url
|
||||
l.vcs_web_url = index.layerItems[id].vcs_web_url
|
||||
l.vcs_web_tree_base_url = index.layerItems[id].vcs_web_tree_base_url
|
||||
l.vcs_web_file_base_url = index.layerItems[id].vcs_web_file_base_url
|
||||
l.save()
|
||||
except Layer.MultipleObjectsReturned:
|
||||
logger.info("Skipped %s as we found multiple layers and "
|
||||
"don't know which to update" %
|
||||
index.layerItems[id].name)
|
||||
|
||||
li_layer_id_to_toaster_layer_id[id] = l.pk
|
||||
|
||||
self.mini_progress("layers", i, total)
|
||||
|
||||
# update layer_versions
|
||||
logger.info("Processing layer versions")
|
||||
|
||||
# Map Layer index layer_branch object id to
|
||||
# layer_version toaster object id
|
||||
li_layer_branch_id_to_toaster_lv_id = {}
|
||||
|
||||
total = len(index.layerBranches)
|
||||
for i, id in enumerate(index.layerBranches):
|
||||
# release as defined by toaster map to layerindex branch
|
||||
release = li_branch_id_to_toaster_release[index.layerBranches[id].branch_id]
|
||||
|
||||
try:
|
||||
lv, created = Layer_Version.objects.get_or_create(
|
||||
layer=Layer.objects.get(
|
||||
pk=li_layer_id_to_toaster_layer_id[index.layerBranches[id].layer_id]),
|
||||
release=release
|
||||
)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
"No such layerindex layer referenced by layerbranch %d" %
|
||||
index.layerBranches[id].layer_id)
|
||||
continue
|
||||
|
||||
if created:
|
||||
lv.release = li_branch_id_to_toaster_release[index.layerBranches[id].branch_id]
|
||||
lv.up_date = index.layerBranches[id].updated
|
||||
lv.commit = index.layerBranches[id].actual_branch
|
||||
lv.dirpath = index.layerBranches[id].vcs_subdir
|
||||
lv.save()
|
||||
|
||||
li_layer_branch_id_to_toaster_lv_id[index.layerBranches[id].id] =\
|
||||
lv.pk
|
||||
self.mini_progress("layer versions", i, total)
|
||||
|
||||
logger.info("Processing layer version dependencies")
|
||||
|
||||
dependlist = {}
|
||||
for id in index.layerDependencies:
|
||||
try:
|
||||
lv = Layer_Version.objects.get(
|
||||
pk=li_layer_branch_id_to_toaster_lv_id[index.layerDependencies[id].layerbranch_id])
|
||||
except Layer_Version.DoesNotExist as e:
|
||||
continue
|
||||
|
||||
if lv not in dependlist:
|
||||
dependlist[lv] = []
|
||||
try:
|
||||
layer_id = li_layer_id_to_toaster_layer_id[index.layerDependencies[id].dependency_id]
|
||||
|
||||
dependlist[lv].append(
|
||||
Layer_Version.objects.get(layer__pk=layer_id,
|
||||
release=lv.release))
|
||||
|
||||
except Layer_Version.DoesNotExist:
|
||||
logger.warning("Cannot find layer version (ls:%s),"
|
||||
"up_id:%s lv:%s" %
|
||||
(self, index.layerDependencies[id].dependency_id, lv))
|
||||
|
||||
total = len(dependlist)
|
||||
for i, lv in enumerate(dependlist):
|
||||
LayerVersionDependency.objects.filter(layer_version=lv).delete()
|
||||
for lvd in dependlist[lv]:
|
||||
LayerVersionDependency.objects.get_or_create(layer_version=lv,
|
||||
depends_on=lvd)
|
||||
self.mini_progress("Layer version dependencies", i, total)
|
||||
|
||||
# update Distros
|
||||
logger.info("Processing distro information")
|
||||
|
||||
total = len(index.distros)
|
||||
for i, id in enumerate(index.distros):
|
||||
distro, created = Distro.objects.get_or_create(
|
||||
name=index.distros[id].name,
|
||||
layer_version=Layer_Version.objects.get(
|
||||
pk=li_layer_branch_id_to_toaster_lv_id[index.distros[id].layerbranch_id]))
|
||||
distro.up_date = index.distros[id].updated
|
||||
distro.name = index.distros[id].name
|
||||
distro.description = index.distros[id].description
|
||||
distro.save()
|
||||
self.mini_progress("distros", i, total)
|
||||
|
||||
# update machines
|
||||
logger.info("Processing machine information")
|
||||
|
||||
total = len(index.machines)
|
||||
for i, id in enumerate(index.machines):
|
||||
mo, created = Machine.objects.get_or_create(
|
||||
name=index.machines[id].name,
|
||||
layer_version=Layer_Version.objects.get(
|
||||
pk=li_layer_branch_id_to_toaster_lv_id[index.machines[id].layerbranch_id]))
|
||||
mo.up_date = index.machines[id].updated
|
||||
mo.name = index.machines[id].name
|
||||
mo.description = index.machines[id].description
|
||||
mo.save()
|
||||
self.mini_progress("machines", i, total)
|
||||
|
||||
# update recipes; paginate by layer version / layer branch
|
||||
logger.info("Processing recipe information")
|
||||
|
||||
total = len(index.recipes)
|
||||
for i, id in enumerate(index.recipes):
|
||||
try:
|
||||
lv_id = li_layer_branch_id_to_toaster_lv_id[index.recipes[id].layerbranch_id]
|
||||
lv = Layer_Version.objects.get(pk=lv_id)
|
||||
|
||||
ro, created = Recipe.objects.get_or_create(
|
||||
layer_version=lv,
|
||||
name=index.recipes[id].pn
|
||||
)
|
||||
|
||||
ro.layer_version = lv
|
||||
ro.up_date = index.recipes[id].updated
|
||||
ro.name = index.recipes[id].pn
|
||||
ro.version = index.recipes[id].pv
|
||||
ro.summary = index.recipes[id].summary
|
||||
ro.description = index.recipes[id].description
|
||||
ro.section = index.recipes[id].section
|
||||
ro.license = index.recipes[id].license
|
||||
ro.homepage = index.recipes[id].homepage
|
||||
ro.bugtracker = index.recipes[id].bugtracker
|
||||
ro.file_path = index.recipes[id].fullpath
|
||||
ro.is_image = 'image' in index.recipes[id].inherits.split()
|
||||
ro.save()
|
||||
except Exception as e:
|
||||
logger.warning("Failed saving recipe %s", e)
|
||||
|
||||
self.mini_progress("recipes", i, total)
|
||||
|
||||
os.system('setterm -cursor on')
|
||||
|
||||
def handle(self, **options):
|
||||
self.update()
|
||||
504
sources/poky/bitbake/lib/toaster/orm/migrations/0001_initial.py
Normal file
504
sources/poky/bitbake/lib/toaster/orm/migrations/0001_initial.py
Normal file
@@ -0,0 +1,504 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BitbakeVersion',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=32)),
|
||||
('giturl', models.URLField()),
|
||||
('branch', models.CharField(max_length=32)),
|
||||
('dirpath', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Branch',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('short_description', models.CharField(max_length=50, blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Branches',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Build',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('machine', models.CharField(max_length=100)),
|
||||
('distro', models.CharField(max_length=100)),
|
||||
('distro_version', models.CharField(max_length=100)),
|
||||
('started_on', models.DateTimeField()),
|
||||
('completed_on', models.DateTimeField()),
|
||||
('outcome', models.IntegerField(default=2, choices=[(0, b'Succeeded'), (1, b'Failed'), (2, b'In Progress')])),
|
||||
('cooker_log_path', models.CharField(max_length=500)),
|
||||
('build_name', models.CharField(max_length=100)),
|
||||
('bitbake_version', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BuildArtifact',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('file_name', models.FilePathField()),
|
||||
('file_size', models.IntegerField()),
|
||||
('build', models.ForeignKey(to='orm.Build', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HelpText',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('area', models.IntegerField(choices=[(0, b'variable')])),
|
||||
('key', models.CharField(max_length=100)),
|
||||
('text', models.TextField()),
|
||||
('build', models.ForeignKey(related_name='helptext_build', to='orm.Build', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Layer',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('layer_index_url', models.URLField()),
|
||||
('vcs_url', models.URLField(default=None, null=True)),
|
||||
('vcs_web_url', models.URLField(default=None, null=True)),
|
||||
('vcs_web_tree_base_url', models.URLField(default=None, null=True)),
|
||||
('vcs_web_file_base_url', models.URLField(default=None, null=True)),
|
||||
('summary', models.TextField(default=None, help_text=b'One-line description of the layer', null=True)),
|
||||
('description', models.TextField(default=None, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Layer_Version',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('branch', models.CharField(max_length=80)),
|
||||
('commit', models.CharField(max_length=100)),
|
||||
('dirpath', models.CharField(default=None, max_length=255, null=True)),
|
||||
('priority', models.IntegerField(default=0)),
|
||||
('local_path', models.FilePathField(default=b'/', max_length=1024)),
|
||||
('build', models.ForeignKey(related_name='layer_version_build', default=None, to='orm.Build', null=True, on_delete=models.CASCADE)),
|
||||
('layer', models.ForeignKey(related_name='layer_version_layer', to='orm.Layer', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LayerSource',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=63)),
|
||||
('sourcetype', models.IntegerField(choices=[(0, b'local'), (1, b'layerindex'), (2, b'imported')])),
|
||||
('apiurl', models.CharField(default=None, max_length=255, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LayerVersionDependency',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('depends_on', models.ForeignKey(related_name='dependees', to='orm.Layer_Version', on_delete=models.CASCADE)),
|
||||
('layer_source', models.ForeignKey(default=None, to='orm.LayerSource', null=True, on_delete=models.CASCADE)),
|
||||
('layer_version', models.ForeignKey(related_name='dependencies', to='orm.Layer_Version', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LogMessage',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('level', models.IntegerField(default=0, choices=[(0, b'info'), (1, b'warn'), (2, b'error'), (3, b'critical'), (-1, b'toaster exception')])),
|
||||
('message', models.TextField(null=True, blank=True)),
|
||||
('pathname', models.FilePathField(max_length=255, blank=True)),
|
||||
('lineno', models.IntegerField(null=True)),
|
||||
('build', models.ForeignKey(to='orm.Build', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Machine',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('layer_source', models.ForeignKey(default=None, to='orm.LayerSource', null=True, on_delete=models.CASCADE)),
|
||||
('layer_version', models.ForeignKey(to='orm.Layer_Version', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Package',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('installed_name', models.CharField(default=b'', max_length=100)),
|
||||
('version', models.CharField(max_length=100, blank=True)),
|
||||
('revision', models.CharField(max_length=32, blank=True)),
|
||||
('summary', models.TextField(blank=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('size', models.IntegerField(default=0)),
|
||||
('installed_size', models.IntegerField(default=0)),
|
||||
('section', models.CharField(max_length=80, blank=True)),
|
||||
('license', models.CharField(max_length=80, blank=True)),
|
||||
('build', models.ForeignKey(to='orm.Build', null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Package_Dependency',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('dep_type', models.IntegerField(choices=[(0, b'depends'), (1, b'depends'), (3, b'recommends'), (2, b'recommends'), (4, b'suggests'), (5, b'provides'), (6, b'replaces'), (7, b'conflicts')])),
|
||||
('depends_on', models.ForeignKey(related_name='package_dependencies_target', to='orm.Package', on_delete=models.CASCADE)),
|
||||
('package', models.ForeignKey(related_name='package_dependencies_source', to='orm.Package', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Package_File',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('path', models.FilePathField(max_length=255, blank=True)),
|
||||
('size', models.IntegerField()),
|
||||
('package', models.ForeignKey(related_name='buildfilelist_package', to='orm.Package', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('short_description', models.CharField(max_length=50, blank=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('user_id', models.IntegerField(null=True)),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('bitbake_version', models.ForeignKey(to='orm.BitbakeVersion', null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectLayer',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('optional', models.BooleanField(default=True)),
|
||||
('layercommit', models.ForeignKey(to='orm.Layer_Version', null=True, on_delete=models.CASCADE)),
|
||||
('project', models.ForeignKey(to='orm.Project', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectTarget',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('target', models.CharField(max_length=100)),
|
||||
('task', models.CharField(max_length=100, null=True)),
|
||||
('project', models.ForeignKey(to='orm.Project', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectVariable',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('value', models.TextField(blank=True)),
|
||||
('project', models.ForeignKey(to='orm.Project', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Recipe',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('name', models.CharField(max_length=100, blank=True)),
|
||||
('version', models.CharField(max_length=100, blank=True)),
|
||||
('summary', models.TextField(blank=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('section', models.CharField(max_length=100, blank=True)),
|
||||
('license', models.CharField(max_length=200, blank=True)),
|
||||
('homepage', models.URLField(blank=True)),
|
||||
('bugtracker', models.URLField(blank=True)),
|
||||
('file_path', models.FilePathField(max_length=255)),
|
||||
('pathflags', models.CharField(max_length=200, blank=True)),
|
||||
('is_image', models.BooleanField(default=False)),
|
||||
('layer_source', models.ForeignKey(default=None, to='orm.LayerSource', null=True, on_delete=models.CASCADE)),
|
||||
('layer_version', models.ForeignKey(related_name='recipe_layer_version', to='orm.Layer_Version', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Recipe_Dependency',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('dep_type', models.IntegerField(choices=[(0, b'depends'), (1, b'rdepends')])),
|
||||
('depends_on', models.ForeignKey(related_name='r_dependencies_depends', to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
('recipe', models.ForeignKey(related_name='r_dependencies_recipe', to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Release',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=32)),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('branch_name', models.CharField(default=b'', max_length=50)),
|
||||
('helptext', models.TextField(null=True)),
|
||||
('bitbake_version', models.ForeignKey(to='orm.BitbakeVersion', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReleaseDefaultLayer',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('layer_name', models.CharField(default=b'', max_length=100)),
|
||||
('release', models.ForeignKey(to='orm.Release', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReleaseLayerSourcePriority',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('priority', models.IntegerField(default=0)),
|
||||
('layer_source', models.ForeignKey(to='orm.LayerSource', on_delete=models.CASCADE)),
|
||||
('release', models.ForeignKey(to='orm.Release', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Target',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('target', models.CharField(max_length=100)),
|
||||
('task', models.CharField(max_length=100, null=True)),
|
||||
('is_image', models.BooleanField(default=False)),
|
||||
('image_size', models.IntegerField(default=0)),
|
||||
('license_manifest_path', models.CharField(max_length=500, null=True)),
|
||||
('build', models.ForeignKey(to='orm.Build', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Target_File',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('path', models.FilePathField()),
|
||||
('size', models.IntegerField()),
|
||||
('inodetype', models.IntegerField(choices=[(1, b'regular'), (2, b'directory'), (3, b'symlink'), (4, b'socket'), (5, b'fifo'), (6, b'character'), (7, b'block')])),
|
||||
('permission', models.CharField(max_length=16)),
|
||||
('owner', models.CharField(max_length=128)),
|
||||
('group', models.CharField(max_length=128)),
|
||||
('directory', models.ForeignKey(related_name='directory_set', to='orm.Target_File', null=True, on_delete=models.CASCADE)),
|
||||
('sym_target', models.ForeignKey(related_name='symlink_set', to='orm.Target_File', null=True, on_delete=models.CASCADE)),
|
||||
('target', models.ForeignKey(to='orm.Target', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Target_Image_File',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('file_name', models.FilePathField(max_length=254)),
|
||||
('file_size', models.IntegerField()),
|
||||
('target', models.ForeignKey(to='orm.Target', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Target_Installed_Package',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('package', models.ForeignKey(related_name='buildtargetlist_package', to='orm.Package', on_delete=models.CASCADE)),
|
||||
('target', models.ForeignKey(to='orm.Target', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('order', models.IntegerField(null=True)),
|
||||
('task_executed', models.BooleanField(default=False)),
|
||||
('outcome', models.IntegerField(default=-1, choices=[(-1, b'Not Available'), (0, b'Succeeded'), (1, b'Covered'), (2, b'Cached'), (3, b'Prebuilt'), (4, b'Failed'), (5, b'Empty')])),
|
||||
('sstate_checksum', models.CharField(max_length=100, blank=True)),
|
||||
('path_to_sstate_obj', models.FilePathField(max_length=500, blank=True)),
|
||||
('task_name', models.CharField(max_length=100)),
|
||||
('source_url', models.FilePathField(max_length=255, blank=True)),
|
||||
('work_directory', models.FilePathField(max_length=255, blank=True)),
|
||||
('script_type', models.IntegerField(default=0, choices=[(0, b'N/A'), (2, b'Python'), (3, b'Shell')])),
|
||||
('line_number', models.IntegerField(default=0)),
|
||||
('disk_io', models.IntegerField(null=True)),
|
||||
('cpu_usage', models.DecimalField(null=True, max_digits=8, decimal_places=2)),
|
||||
('elapsed_time', models.DecimalField(null=True, max_digits=8, decimal_places=2)),
|
||||
('sstate_result', models.IntegerField(default=0, choices=[(0, b'Not Applicable'), (1, b'File not in cache'), (2, b'Failed'), (3, b'Succeeded')])),
|
||||
('message', models.CharField(max_length=240)),
|
||||
('logfile', models.FilePathField(max_length=255, blank=True)),
|
||||
('build', models.ForeignKey(related_name='task_build', to='orm.Build', on_delete=models.CASCADE)),
|
||||
('recipe', models.ForeignKey(related_name='tasks', to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('order', 'recipe'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Task_Dependency',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('depends_on', models.ForeignKey(related_name='task_dependencies_depends', to='orm.Task', on_delete=models.CASCADE)),
|
||||
('task', models.ForeignKey(related_name='task_dependencies_task', to='orm.Task', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ToasterSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=63)),
|
||||
('helptext', models.TextField()),
|
||||
('value', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Variable',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('variable_name', models.CharField(max_length=100)),
|
||||
('variable_value', models.TextField(blank=True)),
|
||||
('changed', models.BooleanField(default=False)),
|
||||
('human_readable_name', models.CharField(max_length=200)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('build', models.ForeignKey(related_name='variable_build', to='orm.Build', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VariableHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('value', models.TextField(blank=True)),
|
||||
('file_name', models.FilePathField(max_length=255)),
|
||||
('line_number', models.IntegerField(null=True)),
|
||||
('operation', models.CharField(max_length=64)),
|
||||
('variable', models.ForeignKey(related_name='vhistory', to='orm.Variable', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='release',
|
||||
field=models.ForeignKey(to='orm.Release', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='package_dependency',
|
||||
name='target',
|
||||
field=models.ForeignKey(to='orm.Target', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='package',
|
||||
name='recipe',
|
||||
field=models.ForeignKey(to='orm.Recipe', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='logmessage',
|
||||
name='task',
|
||||
field=models.ForeignKey(blank=True, to='orm.Task', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layersource',
|
||||
unique_together=set([('sourcetype', 'apiurl')]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='layer_version',
|
||||
name='layer_source',
|
||||
field=models.ForeignKey(default=None, to='orm.LayerSource', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='layer_version',
|
||||
name='project',
|
||||
field=models.ForeignKey(default=None, to='orm.Project', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='layer_version',
|
||||
name='up_branch',
|
||||
field=models.ForeignKey(default=None, to='orm.Branch', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='layer',
|
||||
name='layer_source',
|
||||
field=models.ForeignKey(default=None, to='orm.LayerSource', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='build',
|
||||
name='project',
|
||||
field=models.ForeignKey(to='orm.Project', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='branch',
|
||||
name='layer_source',
|
||||
field=models.ForeignKey(default=True, to='orm.LayerSource', null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImportedLayerSource',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
},
|
||||
bases=('orm.layersource',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LayerIndexLayerSource',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
},
|
||||
bases=('orm.layersource',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LocalLayerSource',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
},
|
||||
bases=('orm.layersource',),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='task',
|
||||
unique_together=set([('build', 'recipe', 'task_name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='releaselayersourcepriority',
|
||||
unique_together=set([('release', 'layer_source')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='recipe',
|
||||
unique_together=set([('layer_version', 'file_path', 'pathflags')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='projectlayer',
|
||||
unique_together=set([('project', 'layercommit')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='machine',
|
||||
unique_together=set([('layer_source', 'up_id')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layerversiondependency',
|
||||
unique_together=set([('layer_source', 'up_id')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layer_version',
|
||||
unique_together=set([('layer_source', 'up_id')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layer',
|
||||
unique_together=set([('layer_source', 'up_id'), ('layer_source', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='branch',
|
||||
unique_together=set([('layer_source', 'up_id'), ('layer_source', 'name')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomImageRecipe',
|
||||
fields=[
|
||||
('recipe_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
('last_updated', models.DateTimeField(default=None, null=True)),
|
||||
('base_recipe', models.ForeignKey(related_name='based_on_recipe', to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
('project', models.ForeignKey(to='orm.Project', on_delete=models.CASCADE)),
|
||||
],
|
||||
bases=('orm.recipe',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0002_customimagerecipe'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomImagePackage',
|
||||
fields=[
|
||||
('package_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='orm.Package', on_delete=models.CASCADE)),
|
||||
('recipe_appends', models.ManyToManyField(related_name='appends_set', to='orm.CustomImageRecipe')),
|
||||
('recipe_excludes', models.ManyToManyField(related_name='excludes_set', to='orm.CustomImageRecipe')),
|
||||
('recipe_includes', models.ManyToManyField(related_name='includes_set', to='orm.CustomImageRecipe')),
|
||||
],
|
||||
bases=('orm.package',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0003_customimagepackage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Provides',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('recipe', models.ForeignKey(to='orm.Recipe', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipe_dependency',
|
||||
name='via',
|
||||
field=models.ForeignKey(null=True, default=None, to='orm.Provides', on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0004_provides'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='cpu_usage',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='cpu_time_system',
|
||||
field=models.DecimalField(null=True, max_digits=8, decimal_places=2),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='cpu_time_user',
|
||||
field=models.DecimalField(null=True, max_digits=8, decimal_places=2),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='disk_io_read',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='disk_io_write',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='ended',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='started',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0005_task_field_separation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='outcome',
|
||||
field=models.IntegerField(default=2, choices=[(0, b'Succeeded'), (1, b'Failed'), (2, b'In Progress'), (3, b'Cancelled')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0006_add_cancelled_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='outcome',
|
||||
field=models.IntegerField(default=2, choices=[(0, 'Succeeded'), (1, 'Failed'), (2, 'In Progress'), (3, 'Cancelled')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='helptext',
|
||||
name='area',
|
||||
field=models.IntegerField(choices=[(0, 'variable')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer',
|
||||
name='summary',
|
||||
field=models.TextField(default=None, null=True, help_text='One-line description of the layer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer_version',
|
||||
name='local_path',
|
||||
field=models.FilePathField(default='/', max_length=1024),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layersource',
|
||||
name='sourcetype',
|
||||
field=models.IntegerField(choices=[(0, 'local'), (1, 'layerindex'), (2, 'imported')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logmessage',
|
||||
name='level',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'info'), (1, 'warn'), (2, 'error'), (3, 'critical'), (-1, 'toaster exception')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='package',
|
||||
name='installed_name',
|
||||
field=models.CharField(default='', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='package_dependency',
|
||||
name='dep_type',
|
||||
field=models.IntegerField(choices=[(0, 'depends'), (1, 'depends'), (3, 'recommends'), (2, 'recommends'), (4, 'suggests'), (5, 'provides'), (6, 'replaces'), (7, 'conflicts')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipe_dependency',
|
||||
name='dep_type',
|
||||
field=models.IntegerField(choices=[(0, 'depends'), (1, 'rdepends')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='release',
|
||||
name='branch_name',
|
||||
field=models.CharField(default='', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='releasedefaultlayer',
|
||||
name='layer_name',
|
||||
field=models.CharField(default='', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='target_file',
|
||||
name='inodetype',
|
||||
field=models.IntegerField(choices=[(1, 'regular'), (2, 'directory'), (3, 'symlink'), (4, 'socket'), (5, 'fifo'), (6, 'character'), (7, 'block')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='outcome',
|
||||
field=models.IntegerField(default=-1, choices=[(-1, 'Not Available'), (0, 'Succeeded'), (1, 'Covered'), (2, 'Cached'), (3, 'Prebuilt'), (4, 'Failed'), (5, 'Empty')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='script_type',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'N/A'), (2, 'Python'), (3, 'Shell')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='sstate_result',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'Not Applicable'), (1, 'File not in cache'), (2, 'Failed'), (3, 'Succeeded')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0007_auto_20160523_1446'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TargetKernelFile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
|
||||
('file_name', models.FilePathField()),
|
||||
('file_size', models.IntegerField()),
|
||||
('target', models.ForeignKey(to='orm.Target', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TargetSDKFile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
|
||||
('file_name', models.FilePathField()),
|
||||
('file_size', models.IntegerField()),
|
||||
('target', models.ForeignKey(to='orm.Target', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='buildartifact',
|
||||
name='build',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BuildArtifact',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0008_refactor_artifact_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='target',
|
||||
name='package_manifest_path',
|
||||
field=models.CharField(null=True, max_length=500),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0009_target_package_manifest_path'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='releaselayersourcepriority',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='releaselayersourcepriority',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='releaselayersourcepriority',
|
||||
name='release',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ImportedLayerSource',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='LayerIndexLayerSource',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='LocalLayerSource',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='recipe',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='recipe',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer',
|
||||
name='up_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer_version',
|
||||
name='layer_source',
|
||||
field=models.IntegerField(default=0, choices=[(0, 'local'), (1, 'layerindex'), (2, 'imported'), (3, 'build')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer_version',
|
||||
name='up_date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='branch',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layer',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layer_version',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='layerversiondependency',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='machine',
|
||||
unique_together=set([]),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ReleaseLayerSourcePriority',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='branch',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='branch',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='layer',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='layer',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='layer_version',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='layerversiondependency',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='layerversiondependency',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='machine',
|
||||
name='layer_source',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='machine',
|
||||
name='up_id',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0010_delete_layer_source_references'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='LayerSource',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def branch_to_release(apps, schema_editor):
|
||||
Layer_Version = apps.get_model('orm', 'Layer_Version')
|
||||
Release = apps.get_model('orm', 'Release')
|
||||
|
||||
print("Converting all layer version up_branches to releases")
|
||||
# Find all the layer versions which have an upbranch and convert them to
|
||||
# the release that they're for.
|
||||
for layer_version in Layer_Version.objects.filter(
|
||||
Q(release=None) & ~Q(up_branch=None)):
|
||||
try:
|
||||
# HEAD and local are equivalent
|
||||
if "HEAD" in layer_version.up_branch.name:
|
||||
release = Release.objects.get(name="local")
|
||||
layer_version.commit = "HEAD"
|
||||
layer_version.branch = "HEAD"
|
||||
else:
|
||||
release = Release.objects.get(
|
||||
name=layer_version.up_branch.name)
|
||||
|
||||
layer_version.release = release
|
||||
layer_version.save()
|
||||
except Exception as e:
|
||||
print("Couldn't work out an appropriate release for %s "
|
||||
"the up_branch was %s "
|
||||
"user the django admin interface to correct it" %
|
||||
(layer_version.layer.name, layer_version.up_branch.name))
|
||||
print(e)
|
||||
|
||||
continue
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0011_delete_layersource'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='layer_version',
|
||||
name='release',
|
||||
field=models.ForeignKey(to='orm.Release', default=None, null=True, on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.RunPython(branch_to_release,
|
||||
reverse_code=migrations.RunPython.noop),
|
||||
|
||||
migrations.RemoveField(
|
||||
model_name='layer_version',
|
||||
name='up_branch',
|
||||
),
|
||||
|
||||
migrations.DeleteModel(
|
||||
name='Branch',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0012_use_release_instead_of_up_branch'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='build',
|
||||
name='recipes_parsed',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='build',
|
||||
name='recipes_to_parse',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0013_recipe_parse_progress_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='build_name',
|
||||
field=models.CharField(default='', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0014_allow_empty_buildname'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='layer',
|
||||
name='local_source_dir',
|
||||
field=models.TextField(null=True, default=None),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0015_layer_local_source_dir'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='build',
|
||||
name='repos_cloned',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='build',
|
||||
name='repos_to_clone',
|
||||
field=models.IntegerField(default=1), # (default off)
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0016_clone_progress'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Distro',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('up_id', models.IntegerField(default=None, null=True)),
|
||||
('up_date', models.DateTimeField(default=None, null=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('layer_version', models.ForeignKey(to='orm.Layer_Version', on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0017_distro_clone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='Project',
|
||||
name='builddir',
|
||||
field=models.TextField(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='Project',
|
||||
name='merged_attr',
|
||||
field=models.BooleanField(default=False)
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='Build',
|
||||
name='progress_item',
|
||||
field=models.CharField(max_length=40)
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.7 on 2019-11-19 03:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0018_project_specific'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='distro',
|
||||
name='up_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='recipes_parsed',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,173 @@
|
||||
# Generated by Django 3.2.12 on 2022-03-06 03:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0019_django_2_2'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bitbakeversion',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='distro',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='helptext',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layer_version',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='layerversiondependency',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logmessage',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='machine',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='package',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='package_dependency',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='package_file',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectlayer',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projecttarget',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projectvariable',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='provides',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipe',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='recipe_dependency',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='release',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='releasedefaultlayer',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='target',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='target_file',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='target_image_file',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='target_installed_package',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='targetkernelfile',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='targetsdkfile',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='task_dependency',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='toastersetting',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='variable',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='variablehistory',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.5 on 2023-11-23 18:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orm', '0020_models_bigautofield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventLogsImports',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('imported', models.BooleanField(default=False)),
|
||||
('build_id', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
1882
sources/poky/bitbake/lib/toaster/orm/models.py
Normal file
1882
sources/poky/bitbake/lib/toaster/orm/models.py
Normal file
File diff suppressed because it is too large
Load Diff
16
sources/poky/bitbake/lib/toaster/pytest.ini
Normal file
16
sources/poky/bitbake/lib/toaster/pytest.ini
Normal file
@@ -0,0 +1,16 @@
|
||||
# -- FILE: pytest.ini (or tox.ini)
|
||||
[pytest]
|
||||
# --create-db - force re creation of the test database
|
||||
# https://pytest-django.readthedocs.io/en/latest/database.html#create-db-force-re-creation-of-the-test-database
|
||||
|
||||
# --html=report.html --self-contained-html
|
||||
# https://docs.pytest.org/en/latest/usage.html#creating-html-reports
|
||||
# https://pytest-html.readthedocs.io/en/latest/user_guide.html#creating-a-self-contained-report
|
||||
addopts = --create-db --html="Toaster Tests Report.html" --self-contained-html
|
||||
|
||||
# Define environment variables using pytest-env
|
||||
# A pytest plugin that enables you to set environment variables in the pytest.ini file.
|
||||
# https://pypi.org/project/pytest-env/
|
||||
env =
|
||||
TOASTER_BUILDSERVER=1
|
||||
DJANGO_SETTINGS_MODULE=toastermain.settings_test
|
||||
0
sources/poky/bitbake/lib/toaster/tests/__init__.py
Normal file
0
sources/poky/bitbake/lib/toaster/tests/__init__.py
Normal file
74
sources/poky/bitbake/lib/toaster/tests/browser/README
Normal file
74
sources/poky/bitbake/lib/toaster/tests/browser/README
Normal file
@@ -0,0 +1,74 @@
|
||||
# Running Toaster's browser-based test suite
|
||||
|
||||
These tests require Selenium to be installed in your Python environment.
|
||||
|
||||
The simplest way to install this is via pip3:
|
||||
|
||||
pip3 install selenium==2.53.2
|
||||
|
||||
Note that if you use other versions of Selenium, some of the tests (such as
|
||||
tests.browser.test_js_unit_tests.TestJsUnitTests) may fail, as these rely on
|
||||
a Selenium test report with a version-specific format.
|
||||
|
||||
To run tests against Chrome:
|
||||
|
||||
* Download chromedriver for your host OS from
|
||||
https://sites.google.com/a/chromium.org/chromedriver/downloads
|
||||
* On *nix systems, put chromedriver on PATH
|
||||
* On Windows, put chromedriver.exe in the same directory as chrome.exe
|
||||
|
||||
To run tests against PhantomJS (headless):
|
||||
--NOTE - Selenium seems to be deprecating support for this mode ---
|
||||
* Download and install PhantomJS:
|
||||
http://phantomjs.org/download.html
|
||||
* On *nix systems, put phantomjs on PATH
|
||||
* Not tested on Windows
|
||||
|
||||
To run tests against Firefox, you may need to install the Marionette driver,
|
||||
depending on how new your version of Firefox is. One clue that you need to do
|
||||
this is if you see an exception like:
|
||||
|
||||
selenium.common.exceptions.WebDriverException: Message: The browser
|
||||
appears to have exited before we could connect. If you specified
|
||||
a log_file in the FirefoxBinary constructor, check it for details.
|
||||
|
||||
See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver
|
||||
for installation instructions. Ensure that the Marionette executable (renamed
|
||||
as wires on Linux or wires.exe on Windows) is on your PATH; and use "marionette"
|
||||
as the browser string passed via TOASTER_TESTS_BROWSER (see below).
|
||||
|
||||
(Note: The Toaster tests have been checked against Firefox 47 with the
|
||||
Marionette driver.)
|
||||
|
||||
The test cases will instantiate a Selenium driver set by the
|
||||
TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified.
|
||||
|
||||
To run tests against the Selenium Firefox Docker container:
|
||||
More explanation is located at https://wiki.yoctoproject.org/wiki/TipsAndTricks/TestingToasterWithContainers
|
||||
* Run the Selenium container:
|
||||
** docker run -it --rm=true -p 5900:5900 -p 4444:4444 --name=selenium selenium/standalone-firefox-debug:2.53.0
|
||||
*** 5900 is the default vnc port. If you are runing a vnc server on your machine map a different port e.g. -p 6900:5900 and connect vnc client to 127.0.0.1:6900
|
||||
*** 4444 is the default selenium sever port.
|
||||
* Run the tests
|
||||
** TOASTER_TESTS_BROWSER=http://127.0.0.1:4444/wd/hub TOASTER_TESTS_URL=http://172.17.0.1:8000 ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser
|
||||
** TOASTER_TESTS_BROWSER=remote TOASTER_REMOTE_HUB=http://127.0.0.1:4444/wd/hub ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser
|
||||
*** TOASTER_REMOTE_HUB - This is the address for the Selenium Remote Web Driver hub. Assuming you ran the contianer with -p 4444:4444 it will be http://127.0.0.1:4444/wd/hub.
|
||||
*** --liveserver=xxx tells Django to run the test server on an interface and port reachable by both host and container.
|
||||
**** 172.17.0.1 is the default docker bridge on linux, viewable from inside and outside the contianers. Find it with "ip -4 addr show dev docker0"
|
||||
* connect to the vnc server to see the tests if you would like
|
||||
** xtightvncviewer 127.0.0.1:5900
|
||||
** note, you need to wait for the test container to come up before this can connect.
|
||||
|
||||
Available drivers:
|
||||
|
||||
* chrome (default)
|
||||
* firefox
|
||||
* marionette (for newer Firefoxes)
|
||||
* ie
|
||||
* phantomjs (deprecated)
|
||||
* remote
|
||||
|
||||
e.g. to run the test suite with phantomjs where you have phantomjs installed
|
||||
in /home/me/apps/phantomjs:
|
||||
|
||||
PATH=/home/me/apps/phantomjs/bin:$PATH TOASTER_TESTS_BROWSER=phantomjs manage.py test tests.browser
|
||||
@@ -0,0 +1,21 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are
|
||||
# modified from Patchwork, released under the same licence terms as Toaster:
|
||||
# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py
|
||||
|
||||
"""
|
||||
Helper methods for creating Toaster Selenium tests which run within
|
||||
the context of Django unit tests.
|
||||
"""
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
|
||||
|
||||
class SeleniumTestCase(SeleniumTestCaseBase, StaticLiveServerTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,267 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are
|
||||
# modified from Patchwork, released under the same licence terms as Toaster:
|
||||
# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py
|
||||
|
||||
"""
|
||||
Helper methods for creating Toaster Selenium tests which run within
|
||||
the context of Django unit tests.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
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, \
|
||||
SessionNotCreatedException
|
||||
|
||||
def create_selenium_driver(cls,browser='chrome'):
|
||||
# set default browser string based on env (if available)
|
||||
env_browser = os.environ.get('TOASTER_TESTS_BROWSER')
|
||||
if env_browser:
|
||||
browser = env_browser
|
||||
|
||||
if browser == 'chrome':
|
||||
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':
|
||||
capabilities = DesiredCapabilities.FIREFOX
|
||||
capabilities['marionette'] = True
|
||||
return webdriver.Firefox(capabilities=capabilities)
|
||||
elif browser == 'ie':
|
||||
return webdriver.Ie()
|
||||
elif browser == 'phantomjs':
|
||||
return webdriver.PhantomJS()
|
||||
elif browser == 'remote':
|
||||
# if we were to add yet another env variable like TOASTER_REMOTE_BROWSER
|
||||
# we could let people pick firefox or chrome, left for later
|
||||
remote_hub= os.environ.get('TOASTER_REMOTE_HUB')
|
||||
driver = webdriver.Remote(remote_hub,
|
||||
webdriver.DesiredCapabilities.FIREFOX.copy())
|
||||
|
||||
driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port))
|
||||
return driver
|
||||
else:
|
||||
msg = 'Selenium driver for browser %s is not available' % browser
|
||||
raise RuntimeError(msg)
|
||||
|
||||
class Wait(WebDriverWait):
|
||||
"""
|
||||
Subclass of WebDriverWait with predetermined timeout and poll
|
||||
frequency. Also deals with a wider variety of exceptions.
|
||||
"""
|
||||
_TIMEOUT = 10
|
||||
_POLL_FREQUENCY = 0.5
|
||||
|
||||
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=''):
|
||||
"""
|
||||
Calls the method provided with the driver as an argument until the
|
||||
return value is not False.
|
||||
"""
|
||||
|
||||
end_time = time.time() + self._timeout
|
||||
while True:
|
||||
try:
|
||||
value = method(self._driver)
|
||||
if value:
|
||||
return value
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
except StaleElementReferenceException:
|
||||
pass
|
||||
|
||||
time.sleep(self._poll)
|
||||
if time.time() > end_time:
|
||||
break
|
||||
|
||||
raise TimeoutException(message)
|
||||
|
||||
def until_not(self, method, message=''):
|
||||
"""
|
||||
Calls the method provided with the driver as an argument until the
|
||||
return value is False.
|
||||
"""
|
||||
|
||||
end_time = time.time() + self._timeout
|
||||
while True:
|
||||
try:
|
||||
value = method(self._driver)
|
||||
if not value:
|
||||
return value
|
||||
except NoSuchElementException:
|
||||
return True
|
||||
except StaleElementReferenceException:
|
||||
pass
|
||||
|
||||
time.sleep(self._poll)
|
||||
if time.time() > end_time:
|
||||
break
|
||||
|
||||
raise TimeoutException(message)
|
||||
|
||||
class SeleniumTestCaseBase(unittest.TestCase):
|
||||
"""
|
||||
NB StaticLiveServerTestCase is used as the base test case so that
|
||||
static files are served correctly in a Selenium test run context; see
|
||||
https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
""" Create a webdriver driver at the class level """
|
||||
|
||||
super(SeleniumTestCaseBase, cls).setUpClass()
|
||||
|
||||
# instantiate the Selenium webdriver once for all the test methods
|
||||
# in this test case
|
||||
cls.driver = create_selenium_driver(cls)
|
||||
cls.driver.maximize_window()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
""" 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):
|
||||
"""
|
||||
Selenium requires absolute URLs, so convert Django URLs returned
|
||||
by resolve() or similar to absolute ones and get using the
|
||||
webdriver instance.
|
||||
|
||||
url: a relative URL
|
||||
"""
|
||||
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)
|
||||
|
||||
def find_all(self, selector):
|
||||
""" Find all elements matching CSS selector """
|
||||
return self.driver.find_elements(By.CSS_SELECTOR, selector)
|
||||
|
||||
def element_exists(self, selector):
|
||||
"""
|
||||
Return True if one element matching selector exists,
|
||||
False otherwise
|
||||
"""
|
||||
return len(self.find_all(selector)) == 1
|
||||
|
||||
def focused_element(self):
|
||||
""" Return the element which currently has focus on the page """
|
||||
return self.driver.switch_to.active_element
|
||||
|
||||
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, 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, 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, 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):
|
||||
""" Wait until element matching CSS selector has focus """
|
||||
is_focused = \
|
||||
lambda driver: self.find(selector) == self.focused_element()
|
||||
msg = 'An element matching "%s" should be focused' % selector
|
||||
Wait(self.driver).until(is_focused, msg)
|
||||
return self.find(selector)
|
||||
|
||||
def enter_text(self, selector, value):
|
||||
""" Insert text into element matching selector """
|
||||
# note that keyup events don't occur until the element is clicked
|
||||
# (in the case of <input type="text"...>, for example), so simulate
|
||||
# user clicking the element before inserting text into it
|
||||
field = self.click(selector)
|
||||
|
||||
field.send_keys(value)
|
||||
return field
|
||||
|
||||
def click(self, selector):
|
||||
""" Click on element which matches CSS selector """
|
||||
element = self.wait_until_visible(selector)
|
||||
element.click()
|
||||
return element
|
||||
|
||||
def get_page_source(self):
|
||||
""" Get raw HTML for the current page """
|
||||
return self.driver.page_source
|
||||
@@ -0,0 +1,476 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
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, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
class TestAllBuildsPage(SeleniumTestCase):
|
||||
""" Tests for all builds page /builds/ """
|
||||
|
||||
PROJECT_NAME = 'test project'
|
||||
CLI_BUILDS_PROJECT_NAME = 'command line builds'
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
|
||||
release=release)
|
||||
self.default_project = Project.objects.create_project(
|
||||
name=self.CLI_BUILDS_PROJECT_NAME,
|
||||
release=release
|
||||
)
|
||||
self.default_project.is_default = True
|
||||
self.default_project.save()
|
||||
|
||||
# parameters for builds to associate with the projects
|
||||
now = timezone.now()
|
||||
|
||||
self.project1_build_success = {
|
||||
'project': self.project1,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.SUCCEEDED
|
||||
}
|
||||
|
||||
self.project1_build_failure = {
|
||||
'project': self.project1,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.FAILED
|
||||
}
|
||||
|
||||
self.default_project_build_success = {
|
||||
'project': self.default_project,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.SUCCEEDED
|
||||
}
|
||||
|
||||
def _get_build_time_element(self, build):
|
||||
"""
|
||||
Return the HTML element containing the build time for a build
|
||||
in the recent builds area
|
||||
"""
|
||||
selector = 'div[data-latest-build-result="%s"] ' \
|
||||
'[data-role="data-recent-build-buildtime-field"]' % build.id
|
||||
|
||||
# because this loads via Ajax, wait for it to be visible
|
||||
self.wait_until_visible(selector)
|
||||
|
||||
build_time_spans = self.find_all(selector)
|
||||
|
||||
self.assertEqual(len(build_time_spans), 1)
|
||||
|
||||
return build_time_spans[0]
|
||||
|
||||
def _get_row_for_build(self, build):
|
||||
""" Get the table row for the build from the all builds table """
|
||||
self.wait_until_visible('#allbuildstable')
|
||||
|
||||
rows = self.find_all('#allbuildstable tr')
|
||||
|
||||
# look for the row with a download link on the recipe which matches the
|
||||
# build ID
|
||||
url = reverse('builddashboard', args=(build.id,))
|
||||
selector = 'td.target a[href="%s"]' % url
|
||||
|
||||
found_row = None
|
||||
for row in rows:
|
||||
|
||||
outcome_links = row.find_elements(By.CSS_SELECTOR, selector)
|
||||
if len(outcome_links) == 1:
|
||||
found_row = row
|
||||
break
|
||||
|
||||
self.assertNotEqual(found_row, None)
|
||||
|
||||
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)
|
||||
target = 'bash'
|
||||
task = 'clean'
|
||||
Target.objects.create(build=build, target=target, task=task)
|
||||
|
||||
url = reverse('all-builds')
|
||||
self.get(url)
|
||||
self.wait_until_visible('td[class="target"]')
|
||||
|
||||
cell = self.find('td[class="target"]')
|
||||
content = cell.get_attribute('innerHTML')
|
||||
expected_text = '%s:%s' % (target, task)
|
||||
|
||||
self.assertTrue(re.search(expected_text, content),
|
||||
'"target" cell should contain text %s' % expected_text)
|
||||
|
||||
def test_rebuild_buttons(self):
|
||||
"""
|
||||
Test 'Rebuild' buttons in recent builds section
|
||||
|
||||
'Rebuild' button should not be shown for command-line builds,
|
||||
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)
|
||||
|
||||
url = reverse('all-builds')
|
||||
self.get(url)
|
||||
|
||||
# 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
|
||||
|
||||
A tooltip should be present next to the command line
|
||||
builds project name in the all builds page, but not for
|
||||
other projects
|
||||
"""
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
Build.objects.create(**self.default_project_build_success)
|
||||
|
||||
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"]')
|
||||
|
||||
selector = 'span.get-help'
|
||||
|
||||
for cell in cells:
|
||||
content = cell.get_attribute('innerHTML')
|
||||
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
|
||||
msg = 'should not be a help icon for non-cli builds name'
|
||||
self.assertEqual(len(help_icons), 0, msg)
|
||||
elif re.search(self.CLI_BUILDS_PROJECT_NAME, content):
|
||||
# help icon next to cli project name
|
||||
msg = 'should be a help icon for cli builds name'
|
||||
self.assertEqual(len(help_icons), 1, msg)
|
||||
else:
|
||||
msg = 'found unexpected project name cell in all builds table'
|
||||
self.fail(msg)
|
||||
|
||||
def test_builds_time_links(self):
|
||||
"""
|
||||
Successful builds should have links on the time column and in the
|
||||
recent builds area; failed builds should not have links on the time column,
|
||||
or in the recent builds area
|
||||
"""
|
||||
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')
|
||||
msg = 'should be a link on the build time for a successful recent build'
|
||||
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')
|
||||
msg = 'should not be a link on the build time for a failed recent build'
|
||||
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')
|
||||
msg = 'should be a link on the build time for a successful build'
|
||||
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')
|
||||
msg = 'should not be a link on the build time for a failed build'
|
||||
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)
|
||||
@@ -0,0 +1,337 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
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/ """
|
||||
|
||||
PROJECT_NAME = 'test project'
|
||||
CLI_BUILDS_PROJECT_NAME = 'command line builds'
|
||||
MACHINE_NAME = 'delorean'
|
||||
|
||||
def setUp(self):
|
||||
""" Add default project manually """
|
||||
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()
|
||||
|
||||
# this project is only set for some of the tests
|
||||
self.project = None
|
||||
|
||||
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()
|
||||
build = Build.objects.create(project=self.default_project,
|
||||
started_on=now,
|
||||
completed_on=now)
|
||||
build.save()
|
||||
|
||||
def _add_non_default_project(self):
|
||||
""" Add another project """
|
||||
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.is_default = False
|
||||
self.project.save()
|
||||
|
||||
# fake the MACHINE variable
|
||||
project_var = ProjectVariable.objects.create(project=self.project,
|
||||
name='MACHINE',
|
||||
value=self.MACHINE_NAME)
|
||||
project_var.save()
|
||||
|
||||
def _get_row_for_project(self, project_name):
|
||||
""" Get the HTML row for a project, or None if not found """
|
||||
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
|
||||
found_row = None
|
||||
for row in rows:
|
||||
if re.search(project_name, row.get_attribute('innerHTML')):
|
||||
found_row = row
|
||||
break
|
||||
|
||||
return found_row
|
||||
|
||||
def test_default_project_hidden(self):
|
||||
"""
|
||||
The default project should be hidden if it has no builds
|
||||
and we should see the "no results" area
|
||||
"""
|
||||
url = reverse('all-projects')
|
||||
self.get(url)
|
||||
self.wait_until_visible('#empty-state-projectstable')
|
||||
|
||||
rows = self.find_all('#projectstable tbody tr')
|
||||
self.assertEqual(len(rows), 0, 'should be no projects displayed')
|
||||
|
||||
def test_default_project_has_build(self):
|
||||
""" The default project should be shown if it has builds """
|
||||
self._add_build_to_default_project()
|
||||
|
||||
url = reverse('all-projects')
|
||||
self.get(url)
|
||||
|
||||
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')
|
||||
|
||||
def test_default_project_release(self):
|
||||
"""
|
||||
The release for the default project should display as
|
||||
'Not applicable'
|
||||
"""
|
||||
# need a build, otherwise project doesn't display at all
|
||||
self._add_build_to_default_project()
|
||||
|
||||
# another project to test, which should show release
|
||||
self._add_non_default_project()
|
||||
|
||||
self.get(reverse('all-projects'))
|
||||
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)
|
||||
|
||||
# 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)
|
||||
text = element.text.strip()
|
||||
self.assertEqual(text, 'Not applicable',
|
||||
'release should be "not applicable" for default project')
|
||||
|
||||
# find the row for the default project
|
||||
other_project_row = self._get_row_for_project(self.project.name)
|
||||
|
||||
# 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)
|
||||
text = element.text.strip()
|
||||
self.assertEqual(text, self.release.name,
|
||||
'release name should be shown for non-default project')
|
||||
|
||||
def test_default_project_machine(self):
|
||||
"""
|
||||
The machine for the default project should display as
|
||||
'Not applicable'
|
||||
"""
|
||||
# need a build, otherwise project doesn't display at all
|
||||
self._add_build_to_default_project()
|
||||
|
||||
# another project to test, which should show machine
|
||||
self._add_non_default_project()
|
||||
|
||||
self.get(reverse('all-projects'))
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
text = element.text.strip()
|
||||
self.assertEqual(text, 'Not applicable',
|
||||
'machine should be not applicable for default project')
|
||||
|
||||
# find the row for the default project
|
||||
other_project_row = self._get_row_for_project(self.project.name)
|
||||
|
||||
# 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)
|
||||
text = element.text.strip()
|
||||
self.assertEqual(text, self.MACHINE_NAME,
|
||||
'machine name should be shown for non-default project')
|
||||
|
||||
def test_project_page_links(self):
|
||||
"""
|
||||
Test that links for the default project point to the builds
|
||||
page /projects/X/builds for that project, and that links for
|
||||
other projects point to their configuration pages /projects/X/
|
||||
"""
|
||||
|
||||
# need a build, otherwise project doesn't display at all
|
||||
self._add_build_to_default_project()
|
||||
|
||||
# another project to test
|
||||
self._add_non_default_project()
|
||||
|
||||
self.get(reverse('all-projects'))
|
||||
|
||||
# find the row for the default project
|
||||
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)
|
||||
link_url = element.get_attribute('href').strip()
|
||||
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)
|
||||
|
||||
# find the row for the other project
|
||||
other_project_row = self._get_row_for_project(self.project.name)
|
||||
|
||||
# check the link for the other project
|
||||
selector = 'span[data-project-field="name"] a'
|
||||
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)
|
||||
@@ -0,0 +1,340 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
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):
|
||||
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)
|
||||
project = Project.objects.create_project(name='test project',
|
||||
release=release)
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
self.build1 = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=now,
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
self.build2 = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=now,
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
self.build3 = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=now,
|
||||
outcome=Build.FAILED)
|
||||
|
||||
# add Variable objects to the successful builds, as this is the criterion
|
||||
# used to determine whether the left-hand panel should be displayed
|
||||
Variable.objects.create(build=self.build1,
|
||||
variable_name='Foo',
|
||||
variable_value='Bar')
|
||||
Variable.objects.create(build=self.build2,
|
||||
variable_name='Foo',
|
||||
variable_value='Bar')
|
||||
|
||||
# exception
|
||||
msg1 = 'an exception was thrown'
|
||||
self.exception_message = LogMessage.objects.create(
|
||||
build=self.build1,
|
||||
level=LogMessage.EXCEPTION,
|
||||
message=msg1
|
||||
)
|
||||
|
||||
# critical
|
||||
msg2 = 'a critical error occurred'
|
||||
self.critical_message = LogMessage.objects.create(
|
||||
build=self.build1,
|
||||
level=LogMessage.CRITICAL,
|
||||
message=msg2
|
||||
)
|
||||
|
||||
# error on the failed build
|
||||
msg3 = 'an error occurred'
|
||||
self.error_message = LogMessage.objects.create(
|
||||
build=self.build3,
|
||||
level=LogMessage.ERROR,
|
||||
message=msg3
|
||||
)
|
||||
|
||||
# warning on the failed build
|
||||
msg4 = 'DANGER WILL ROBINSON'
|
||||
self.warning_message = LogMessage.objects.create(
|
||||
build=self.build3,
|
||||
level=LogMessage.WARNING,
|
||||
message=msg4
|
||||
)
|
||||
|
||||
# recipes related to the build, for testing the edit custom image/new
|
||||
# custom image buttons
|
||||
layer = Layer.objects.create(name='alayer')
|
||||
layer_version = Layer_Version.objects.create(
|
||||
layer=layer, build=self.build1
|
||||
)
|
||||
|
||||
# non-image recipes related to a build, for testing the new custom
|
||||
# image button
|
||||
layer_version2 = Layer_Version.objects.create(layer=layer,
|
||||
build=self.build3)
|
||||
|
||||
# image recipes
|
||||
self.image_recipe1 = Recipe.objects.create(
|
||||
name='recipeA',
|
||||
layer_version=layer_version,
|
||||
file_path='/foo/recipeA.bb',
|
||||
is_image=True
|
||||
)
|
||||
self.image_recipe2 = Recipe.objects.create(
|
||||
name='recipeB',
|
||||
layer_version=layer_version,
|
||||
file_path='/foo/recipeB.bb',
|
||||
is_image=True
|
||||
)
|
||||
|
||||
# custom image recipes for this project
|
||||
self.custom_image_recipe1 = CustomImageRecipe.objects.create(
|
||||
name='customRecipeY',
|
||||
project=project,
|
||||
layer_version=layer_version,
|
||||
file_path='/foo/customRecipeY.bb',
|
||||
base_recipe=self.image_recipe1,
|
||||
is_image=True
|
||||
)
|
||||
self.custom_image_recipe2 = CustomImageRecipe.objects.create(
|
||||
name='customRecipeZ',
|
||||
project=project,
|
||||
layer_version=layer_version,
|
||||
file_path='/foo/customRecipeZ.bb',
|
||||
base_recipe=self.image_recipe2,
|
||||
is_image=True
|
||||
)
|
||||
|
||||
# custom image recipe for a different project (to test filtering
|
||||
# of image recipes and custom image recipes is correct: this shouldn't
|
||||
# show up in either query against self.build1)
|
||||
self.custom_image_recipe3 = CustomImageRecipe.objects.create(
|
||||
name='customRecipeOmega',
|
||||
project=Project.objects.create(name='baz', release=release),
|
||||
layer_version=Layer_Version.objects.create(
|
||||
layer=layer, build=self.build2
|
||||
),
|
||||
file_path='/foo/customRecipeOmega.bb',
|
||||
base_recipe=self.image_recipe2,
|
||||
is_image=True
|
||||
)
|
||||
|
||||
# another non-image recipe (to test filtering of image recipes and
|
||||
# custom image recipes is correct: this shouldn't show up in either
|
||||
# for any build)
|
||||
self.non_image_recipe = Recipe.objects.create(
|
||||
name='nonImageRecipe',
|
||||
layer_version=layer_version,
|
||||
file_path='/foo/nonImageRecipe.bb',
|
||||
is_image=False
|
||||
)
|
||||
|
||||
def _get_build_dashboard(self, build):
|
||||
"""
|
||||
Navigate to the build dashboard for build
|
||||
"""
|
||||
url = reverse('builddashboard', args=(build.id,))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#global-nav', poll=3)
|
||||
|
||||
def _get_build_dashboard_errors(self, build):
|
||||
"""
|
||||
Get a list of HTML fragments representing the errors on the
|
||||
dashboard for the Build object build
|
||||
"""
|
||||
self._get_build_dashboard(build)
|
||||
return self.find_all('#errors div.alert-danger')
|
||||
|
||||
def _check_for_log_message(self, message_elements, log_message):
|
||||
"""
|
||||
Check that the LogMessage <log_message> has a representation in
|
||||
the HTML elements <message_elements>.
|
||||
|
||||
message_elements: WebElements representing the log messages shown
|
||||
in the build dashboard; each should have a <pre> element inside
|
||||
it with a data-log-message-id attribute
|
||||
|
||||
log_message: orm.models.LogMessage instance
|
||||
"""
|
||||
expected_text = log_message.message
|
||||
expected_pk = str(log_message.pk)
|
||||
|
||||
found = False
|
||||
for element in message_elements:
|
||||
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')
|
||||
id_matches = (log_message_pk == expected_pk)
|
||||
|
||||
if text_matches and id_matches:
|
||||
found = True
|
||||
break
|
||||
|
||||
template_vars = (expected_text, expected_pk)
|
||||
assertion_failed_msg = 'message not found: ' \
|
||||
'expected text "%s" and ID %s' % template_vars
|
||||
self.assertTrue(found, assertion_failed_msg)
|
||||
|
||||
def _check_for_error_message(self, build, log_message):
|
||||
"""
|
||||
Check whether the LogMessage instance <log_message> is
|
||||
represented as an HTML error in the dashboard page for the Build object
|
||||
build
|
||||
"""
|
||||
errors = self._get_build_dashboard_errors(build)
|
||||
self._check_for_log_message(errors, log_message)
|
||||
|
||||
def _check_labels_in_modal(self, modal, expected):
|
||||
"""
|
||||
Check that the text values of the <label> elements inside
|
||||
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_text = [lab.text for lab in labels]
|
||||
self.assertEqual(len(labels_text), len(expected))
|
||||
|
||||
for expected_text in expected:
|
||||
self.assertTrue(expected_text in labels_text,
|
||||
"Could not find %s in %s" % (expected_text,
|
||||
labels_text))
|
||||
|
||||
def test_exceptions_show_as_errors(self):
|
||||
"""
|
||||
LogMessages with level EXCEPTION should display in the errors
|
||||
section of the page
|
||||
"""
|
||||
self._check_for_error_message(self.build1, self.exception_message)
|
||||
|
||||
def test_criticals_show_as_errors(self):
|
||||
"""
|
||||
LogMessages with level CRITICAL should display in the errors
|
||||
section of the page
|
||||
"""
|
||||
self._check_for_error_message(self.build1, self.critical_message)
|
||||
|
||||
def test_edit_custom_image_button(self):
|
||||
"""
|
||||
A build which built two custom images should present a modal which lets
|
||||
the user choose one of them to edit
|
||||
"""
|
||||
self._get_build_dashboard(self.build1)
|
||||
|
||||
# click the "edit custom image" button, which populates the modal
|
||||
selector = '[data-role="edit-custom-image-trigger"]'
|
||||
self.click(selector)
|
||||
|
||||
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
|
||||
expected_recipes = [
|
||||
self.custom_image_recipe1.name,
|
||||
self.custom_image_recipe2.name
|
||||
]
|
||||
|
||||
self._check_labels_in_modal(modal, expected_recipes)
|
||||
|
||||
def test_new_custom_image_button(self):
|
||||
"""
|
||||
Check that a build with multiple images and custom images presents
|
||||
all of them as options for creating a new custom image from
|
||||
"""
|
||||
self._get_build_dashboard(self.build1)
|
||||
|
||||
# click the "new custom image" button, which populates the modal
|
||||
selector = '[data-role="new-custom-image-trigger"]'
|
||||
self.click(selector)
|
||||
|
||||
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
|
||||
expected_recipes = [
|
||||
self.image_recipe1.name,
|
||||
self.image_recipe2.name,
|
||||
self.custom_image_recipe1.name,
|
||||
self.custom_image_recipe2.name
|
||||
]
|
||||
|
||||
self._check_labels_in_modal(modal, expected_recipes)
|
||||
|
||||
def test_new_custom_image_button_no_image(self):
|
||||
"""
|
||||
Check that a build which builds non-image recipes doesn't show
|
||||
the new custom image button on the dashboard.
|
||||
"""
|
||||
self._get_build_dashboard(self.build3)
|
||||
selector = '[data-role="new-custom-image-trigger"]'
|
||||
self.assertFalse(self.element_exists(selector),
|
||||
'new custom image button should not show for builds which ' \
|
||||
'don\'t have any image recipes')
|
||||
|
||||
def test_left_panel(self):
|
||||
""""
|
||||
Builds which succeed should have a left panel and a build summary
|
||||
"""
|
||||
self._get_build_dashboard(self.build1)
|
||||
|
||||
left_panel = self.find_all('#nav')
|
||||
self.assertEqual(len(left_panel), 1)
|
||||
|
||||
build_summary = self.find_all('[data-role="build-summary-heading"]')
|
||||
self.assertEqual(len(build_summary), 1)
|
||||
|
||||
def test_failed_no_left_panel(self):
|
||||
"""
|
||||
Builds which fail should have no left panel and no build summary
|
||||
"""
|
||||
self._get_build_dashboard(self.build3)
|
||||
|
||||
left_panel = self.find_all('#nav')
|
||||
self.assertEqual(len(left_panel), 0)
|
||||
|
||||
build_summary = self.find_all('[data-role="build-summary-heading"]')
|
||||
self.assertEqual(len(build_summary), 0)
|
||||
|
||||
def test_failed_shows_errors_and_warnings(self):
|
||||
"""
|
||||
Failed builds should still show error and warning messages
|
||||
"""
|
||||
self._get_build_dashboard(self.build3)
|
||||
|
||||
errors = self.find_all('#errors div.alert-danger')
|
||||
self._check_for_log_message(errors, self.error_message)
|
||||
|
||||
# expand the warnings area
|
||||
self.click('#warning-toggle')
|
||||
self.wait_until_visible('#warnings div.alert-warning')
|
||||
|
||||
warnings = self.find_all('#warnings div.alert-warning')
|
||||
self._check_for_log_message(warnings, self.warning_message)
|
||||
@@ -0,0 +1,212 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from tests.browser.selenium_helpers import SeleniumTestCase
|
||||
|
||||
from orm.models import Project, Release, BitbakeVersion, Build, Target, Package
|
||||
from orm.models import Target_Image_File, TargetSDKFile, TargetKernelFile
|
||||
from orm.models import Target_Installed_Package, Variable
|
||||
|
||||
class TestBuildDashboardPageArtifacts(SeleniumTestCase):
|
||||
""" Tests for artifacts on the build dashboard /build/X """
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
self.project = Project.objects.create_project(name='test project',
|
||||
release=release)
|
||||
|
||||
def _get_build_dashboard(self, build):
|
||||
"""
|
||||
Navigate to the build dashboard for build
|
||||
"""
|
||||
url = reverse('builddashboard', args=(build.id,))
|
||||
self.get(url)
|
||||
|
||||
def _has_build_artifacts_heading(self):
|
||||
"""
|
||||
Check whether the "Build artifacts" heading is visible (True if it
|
||||
is, False otherwise).
|
||||
"""
|
||||
return self.element_exists('[data-heading="build-artifacts"]')
|
||||
|
||||
def _has_images_menu_option(self):
|
||||
"""
|
||||
Try to get the "Images" list element from the left-hand menu in the
|
||||
build dashboard, and return True if it is present, False otherwise.
|
||||
"""
|
||||
return self.element_exists('li.nav-header[data-menu-heading="images"]')
|
||||
|
||||
def test_no_artifacts(self):
|
||||
"""
|
||||
If a build produced no artifacts, the artifacts heading and images
|
||||
menu option shouldn't show.
|
||||
"""
|
||||
now = timezone.now()
|
||||
build = Build.objects.create(project=self.project,
|
||||
started_on=now, completed_on=now, outcome=Build.SUCCEEDED)
|
||||
|
||||
Target.objects.create(is_image=False, build=build, task='',
|
||||
target='mpfr-native')
|
||||
|
||||
self._get_build_dashboard(build)
|
||||
|
||||
# check build artifacts heading
|
||||
msg = 'Build artifacts heading should not be displayed for non-image' \
|
||||
'builds'
|
||||
self.assertFalse(self._has_build_artifacts_heading(), msg)
|
||||
|
||||
# check "Images" option in left-hand menu (should not be there)
|
||||
msg = 'Images option should not be shown in left-hand menu'
|
||||
self.assertFalse(self._has_images_menu_option(), msg)
|
||||
|
||||
def test_sdk_artifacts(self):
|
||||
"""
|
||||
If a build produced SDK artifacts, they should be shown, but the section
|
||||
for image files and the images menu option should be hidden.
|
||||
|
||||
The packages count and size should also be hidden.
|
||||
"""
|
||||
now = timezone.now()
|
||||
build = Build.objects.create(project=self.project,
|
||||
started_on=now, completed_on=timezone.now(),
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
target = Target.objects.create(is_image=True, build=build,
|
||||
task='populate_sdk', target='core-image-minimal')
|
||||
|
||||
sdk_file1 = TargetSDKFile.objects.create(target=target,
|
||||
file_size=100000,
|
||||
file_name='/home/foo/core-image-minimal.toolchain.sh')
|
||||
|
||||
sdk_file2 = TargetSDKFile.objects.create(target=target,
|
||||
file_size=120000,
|
||||
file_name='/home/foo/x86_64.toolchain.sh')
|
||||
|
||||
self._get_build_dashboard(build)
|
||||
|
||||
# check build artifacts heading
|
||||
msg = 'Build artifacts heading should be displayed for SDK ' \
|
||||
'builds which generate artifacts'
|
||||
self.assertTrue(self._has_build_artifacts_heading(), msg)
|
||||
|
||||
# check "Images" option in left-hand menu (should not be there)
|
||||
msg = 'Images option should not be shown in left-hand menu for ' \
|
||||
'builds which didn\'t generate an image file'
|
||||
self.assertFalse(self._has_images_menu_option(), msg)
|
||||
|
||||
# check links to SDK artifacts
|
||||
sdk_artifact_links = self.find_all('[data-links="sdk-artifacts"] li')
|
||||
self.assertEqual(len(sdk_artifact_links), 2,
|
||||
'should be links to 2 SDK artifacts')
|
||||
|
||||
# package count and size should not be visible, no link on
|
||||
# target name
|
||||
selector = '[data-value="target-package-count"]'
|
||||
self.assertFalse(self.element_exists(selector),
|
||||
'package count should not be shown for non-image builds')
|
||||
|
||||
selector = '[data-value="target-package-size"]'
|
||||
self.assertFalse(self.element_exists(selector),
|
||||
'package size should not be shown for non-image builds')
|
||||
|
||||
selector = '[data-link="target-packages"]'
|
||||
self.assertFalse(self.element_exists(selector),
|
||||
'link to target packages should not be on target heading')
|
||||
|
||||
def test_image_artifacts(self):
|
||||
"""
|
||||
If a build produced image files, kernel artifacts, and manifests,
|
||||
they should all be shown, as well as the image link in the left-hand
|
||||
menu.
|
||||
|
||||
The packages count and size should be shown, with a link to the
|
||||
package display page.
|
||||
"""
|
||||
now = timezone.now()
|
||||
build = Build.objects.create(project=self.project,
|
||||
started_on=now, completed_on=timezone.now(),
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
# add a variable to the build so that it counts as "started"
|
||||
Variable.objects.create(build=build,
|
||||
variable_name='Christopher',
|
||||
variable_value='Lee')
|
||||
|
||||
target = Target.objects.create(is_image=True, build=build,
|
||||
task='', target='core-image-minimal',
|
||||
license_manifest_path='/home/foo/license.manifest',
|
||||
package_manifest_path='/home/foo/package.manifest')
|
||||
|
||||
image_file = Target_Image_File.objects.create(target=target,
|
||||
file_name='/home/foo/core-image-minimal.ext4', file_size=9000)
|
||||
|
||||
kernel_file1 = TargetKernelFile.objects.create(target=target,
|
||||
file_name='/home/foo/bzImage', file_size=2000)
|
||||
|
||||
kernel_file2 = TargetKernelFile.objects.create(target=target,
|
||||
file_name='/home/foo/bzImage', file_size=2000)
|
||||
|
||||
package = Package.objects.create(build=build, name='foo', size=1024,
|
||||
installed_name='foo1')
|
||||
installed_package = Target_Installed_Package.objects.create(
|
||||
target=target, package=package)
|
||||
|
||||
self._get_build_dashboard(build)
|
||||
|
||||
# check build artifacts heading
|
||||
msg = 'Build artifacts heading should be displayed for image ' \
|
||||
'builds'
|
||||
self.assertTrue(self._has_build_artifacts_heading(), msg)
|
||||
|
||||
# check "Images" option in left-hand menu (should be there)
|
||||
msg = 'Images option should be shown in left-hand menu for image builds'
|
||||
self.assertTrue(self._has_images_menu_option(), msg)
|
||||
|
||||
# check link to image file
|
||||
selector = '[data-links="image-artifacts"] li'
|
||||
self.assertTrue(self.element_exists(selector),
|
||||
'should be a link to the image file (selector %s)' % selector)
|
||||
|
||||
# check links to kernel artifacts
|
||||
kernel_artifact_links = \
|
||||
self.find_all('[data-links="kernel-artifacts"] li')
|
||||
self.assertEqual(len(kernel_artifact_links), 2,
|
||||
'should be links to 2 kernel artifacts')
|
||||
|
||||
# check manifest links
|
||||
selector = 'a[data-link="license-manifest"]'
|
||||
self.assertTrue(self.element_exists(selector),
|
||||
'should be a link to the license manifest (selector %s)' % selector)
|
||||
|
||||
selector = 'a[data-link="package-manifest"]'
|
||||
self.assertTrue(self.element_exists(selector),
|
||||
'should be a link to the package manifest (selector %s)' % selector)
|
||||
|
||||
# check package count and size, link on target name
|
||||
selector = '[data-value="target-package-count"]'
|
||||
element = self.find(selector)
|
||||
self.assertEqual(element.text, '1',
|
||||
'package count should be shown for image builds')
|
||||
|
||||
selector = '[data-value="target-package-size"]'
|
||||
element = self.find(selector)
|
||||
self.assertEqual(element.text, '1.0 KB',
|
||||
'package size should be shown for image builds')
|
||||
|
||||
selector = '[data-link="target-packages"]'
|
||||
self.assertTrue(self.element_exists(selector),
|
||||
'link to target packages should be on target heading')
|
||||
@@ -0,0 +1,54 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
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
|
||||
from orm.models import Target
|
||||
|
||||
class TestBuilddashboardPageRecipes(SeleniumTestCase):
|
||||
""" Test build dashboard recipes sub-page """
|
||||
|
||||
def setUp(self):
|
||||
project = Project.objects.get_or_create_default_project()
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
self.build = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=now)
|
||||
|
||||
layer = Layer.objects.create()
|
||||
|
||||
layer_version = Layer_Version.objects.create(layer=layer,
|
||||
build=self.build)
|
||||
|
||||
recipe = Recipe.objects.create(layer_version=layer_version)
|
||||
|
||||
task = Task.objects.create(build=self.build, recipe=recipe, order=1)
|
||||
|
||||
Target.objects.create(build=self.build, task=task, target='do_build')
|
||||
|
||||
def test_build_recipes_columns(self):
|
||||
"""
|
||||
Check that non-hideable columns of the table on the recipes sub-page
|
||||
are disabled on the edit columns dropdown.
|
||||
"""
|
||||
url = reverse('recipes', args=(self.build.id,))
|
||||
self.get(url)
|
||||
|
||||
self.wait_until_visible('#edit-columns-button')
|
||||
|
||||
# check that options for the non-hideable columns are disabled
|
||||
non_hideable = ['name', 'version']
|
||||
|
||||
for column in non_hideable:
|
||||
selector = 'input#checkbox-%s[disabled="disabled"]' % column
|
||||
self.wait_until_present(selector)
|
||||
@@ -0,0 +1,53 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
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
|
||||
from orm.models import Target
|
||||
|
||||
class TestBuilddashboardPageTasks(SeleniumTestCase):
|
||||
""" Test build dashboard tasks sub-page """
|
||||
|
||||
def setUp(self):
|
||||
project = Project.objects.get_or_create_default_project()
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
self.build = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=now)
|
||||
|
||||
layer = Layer.objects.create()
|
||||
|
||||
layer_version = Layer_Version.objects.create(layer=layer)
|
||||
|
||||
recipe = Recipe.objects.create(layer_version=layer_version)
|
||||
|
||||
task = Task.objects.create(build=self.build, recipe=recipe, order=1)
|
||||
|
||||
Target.objects.create(build=self.build, task=task, target='do_build')
|
||||
|
||||
def test_build_tasks_columns(self):
|
||||
"""
|
||||
Check that non-hideable columns of the table on the tasks sub-page
|
||||
are disabled on the edit columns dropdown.
|
||||
"""
|
||||
url = reverse('tasks', args=(self.build.id,))
|
||||
self.get(url)
|
||||
|
||||
self.wait_until_visible('#edit-columns-button')
|
||||
|
||||
# check that options for the non-hideable columns are disabled
|
||||
non_hideable = ['order', 'task_name', 'recipe__name']
|
||||
|
||||
for column in non_hideable:
|
||||
selector = 'input#checkbox-%s[disabled="disabled"]' % column
|
||||
self.wait_until_present(selector)
|
||||
@@ -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")
|
||||
@@ -0,0 +1,45 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
"""
|
||||
Run the js unit tests
|
||||
"""
|
||||
|
||||
from django.urls import reverse
|
||||
from tests.browser.selenium_helpers import SeleniumTestCase
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
|
||||
class TestJsUnitTests(SeleniumTestCase):
|
||||
""" Test landing page shows the Toaster brand """
|
||||
|
||||
fixtures = ['toastergui-unittest-data']
|
||||
|
||||
def test_that_js_unit_tests_pass(self):
|
||||
url = reverse('js-unit-tests')
|
||||
self.get(url)
|
||||
self.wait_until_present('#qunit-testresult .failed')
|
||||
|
||||
failed = self.find("#qunit-testresult .failed").text
|
||||
passed = self.find("#qunit-testresult .passed").text
|
||||
total = self.find("#qunit-testresult .total").text
|
||||
|
||||
logger.info("Js unit tests completed %s out of %s passed, %s failed",
|
||||
passed,
|
||||
total,
|
||||
failed)
|
||||
|
||||
failed_tests = self.find_all("li .fail .test-message")
|
||||
for fail in failed_tests:
|
||||
logger.error("JS unit test failed: %s" % fail.text)
|
||||
|
||||
self.assertEqual(failed, '0',
|
||||
"%s JS unit tests failed" % failed)
|
||||
@@ -0,0 +1,221 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestLandingPage(SeleniumTestCase):
|
||||
""" Tests for redirects on the landing page """
|
||||
|
||||
PROJECT_NAME = 'test project'
|
||||
LANDING_PAGE_TITLE = 'This is Toaster'
|
||||
CLI_BUILDS_PROJECT_NAME = 'command line builds'
|
||||
|
||||
def setUp(self):
|
||||
""" Add default project manually """
|
||||
self.project = Project.objects.create_project(
|
||||
self.CLI_BUILDS_PROJECT_NAME,
|
||||
None
|
||||
)
|
||||
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
|
||||
=> should see the landing page
|
||||
"""
|
||||
self.get(reverse('landing'))
|
||||
self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source())
|
||||
|
||||
def test_default_project_has_build(self):
|
||||
"""
|
||||
Default project has a build, no other projects
|
||||
=> should see the builds page
|
||||
"""
|
||||
now = timezone.now()
|
||||
build = Build.objects.create(project=self.project,
|
||||
started_on=now,
|
||||
completed_on=now)
|
||||
build.save()
|
||||
|
||||
self.get(reverse('landing'))
|
||||
|
||||
elements = self.find_all('#allbuildstable')
|
||||
self.assertEqual(len(elements), 1, 'should redirect to builds')
|
||||
content = self.get_page_source()
|
||||
self.assertFalse(self.PROJECT_NAME in content,
|
||||
'should not show builds for project %s' % self.PROJECT_NAME)
|
||||
self.assertTrue(self.CLI_BUILDS_PROJECT_NAME in content,
|
||||
'should show builds for cli project')
|
||||
|
||||
def test_user_project_exists(self):
|
||||
"""
|
||||
User has added a project (without builds)
|
||||
=> should see the projects page
|
||||
"""
|
||||
user_project = Project.objects.create_project('foo', None)
|
||||
user_project.save()
|
||||
|
||||
self.get(reverse('landing'))
|
||||
|
||||
elements = self.find_all('#projectstable')
|
||||
self.assertEqual(len(elements), 1, 'should redirect to projects')
|
||||
|
||||
def test_user_project_has_build(self):
|
||||
"""
|
||||
User has added a project (with builds), command line builds doesn't
|
||||
=> should see the builds page
|
||||
"""
|
||||
user_project = Project.objects.create_project(self.PROJECT_NAME, None)
|
||||
user_project.save()
|
||||
|
||||
now = timezone.now()
|
||||
build = Build.objects.create(project=user_project,
|
||||
started_on=now,
|
||||
completed_on=now)
|
||||
build.save()
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,237 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
|
||||
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
|
||||
from orm.models import BitbakeVersion
|
||||
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
class TestLayerDetailsPage(SeleniumTestCase):
|
||||
""" Test layerdetails page works correctly """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestLayerDetailsPage, self).__init__(*args, **kwargs)
|
||||
|
||||
self.initial_values = None
|
||||
self.url = None
|
||||
self.imported_layer_version = None
|
||||
|
||||
def setUp(self):
|
||||
release = Release.objects.create(
|
||||
name='baz',
|
||||
bitbake_version=BitbakeVersion.objects.create(name='v1')
|
||||
)
|
||||
|
||||
# project to add new custom images to
|
||||
self.project = Project.objects.create(name='foo', release=release)
|
||||
|
||||
name = "meta-imported"
|
||||
vcs_url = "git://example.com/meta-imported"
|
||||
subdir = "/layer"
|
||||
gitrev = "d33d"
|
||||
summary = "A imported layer"
|
||||
description = "This was imported"
|
||||
|
||||
imported_layer = Layer.objects.create(name=name,
|
||||
vcs_url=vcs_url,
|
||||
summary=summary,
|
||||
description=description)
|
||||
|
||||
self.imported_layer_version = Layer_Version.objects.create(
|
||||
layer=imported_layer,
|
||||
layer_source=LayerSource.TYPE_IMPORTED,
|
||||
branch=gitrev,
|
||||
commit=gitrev,
|
||||
dirpath=subdir,
|
||||
project=self.project)
|
||||
|
||||
self.initial_values = [name, vcs_url, subdir, gitrev, summary,
|
||||
description]
|
||||
self.url = reverse('layerdetails',
|
||||
args=(self.project.pk,
|
||||
self.imported_layer_version.pk))
|
||||
|
||||
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")
|
||||
self.click("#repo")
|
||||
|
||||
self.wait_until_visible("#layer-git-repo-url")
|
||||
|
||||
# Open every edit box
|
||||
for btn in self.find_all("dd .glyphicon-edit"):
|
||||
btn.click()
|
||||
|
||||
# Wait for the inputs to become visible after animation
|
||||
self.wait_until_visible("#layer-git input[type=text]")
|
||||
self.wait_until_visible("dd textarea")
|
||||
self.wait_until_visible("dd .change-btn")
|
||||
|
||||
# Edit each value
|
||||
for inputs in self.find_all("#layer-git input[type=text]") + \
|
||||
self.find_all("dd textarea"):
|
||||
# ignore the tt inputs (twitter typeahead input)
|
||||
if "tt-" in inputs.get_attribute("class"):
|
||||
continue
|
||||
|
||||
value = inputs.get_attribute("value")
|
||||
|
||||
self.assertTrue(value in self.initial_values,
|
||||
"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()
|
||||
|
||||
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
|
||||
self.get(self.url)
|
||||
|
||||
new_values = ["%s-edited" % old_val
|
||||
for old_val in self.initial_values]
|
||||
|
||||
for inputs in self.find_all('#layer-git input[type="text"]') + \
|
||||
self.find_all('dd textarea'):
|
||||
# ignore the tt inputs (twitter typeahead input)
|
||||
if "tt-" in inputs.get_attribute("class"):
|
||||
continue
|
||||
|
||||
value = inputs.get_attribute("value")
|
||||
|
||||
self.assertTrue(value in new_values,
|
||||
"Expecting any of \"%s\" but got \"%s\"" %
|
||||
(new_values, value))
|
||||
|
||||
# Now convert it to a local layer
|
||||
self.click("#edit-layer-source")
|
||||
self.click("#dir")
|
||||
dir_input = self.wait_until_visible("#layer-dir-path-in-details")
|
||||
|
||||
new_dir = "/home/test/my-meta-dir"
|
||||
dir_input.send_keys(new_dir)
|
||||
|
||||
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
|
||||
self.get(self.url)
|
||||
dir_input = self.find("#layer-dir-path-in-details")
|
||||
self.assertTrue(new_dir in dir_input.get_attribute("value"),
|
||||
"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 """
|
||||
|
||||
self.get(self.url)
|
||||
|
||||
# Wait for the tables to load to avoid a race condition where the
|
||||
# toaster tables have made an async request. If the layer is deleted
|
||||
# before the request finishes it will cause an exception and fail this
|
||||
# test.
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
|
||||
wait.until(EC.text_to_be_present_in_element(
|
||||
(By.CLASS_NAME,
|
||||
"table-count-recipestable"), "0"))
|
||||
|
||||
wait.until(EC.text_to_be_present_in_element(
|
||||
(By.CLASS_NAME,
|
||||
"table-count-machinestable"), "0"))
|
||||
|
||||
self.click('a[data-target="#delete-layer-modal"]')
|
||||
self.wait_until_visible("#delete-layer-modal")
|
||||
self.click("#layer-delete-confirmed")
|
||||
|
||||
notification = self.wait_until_visible("#change-notification-msg")
|
||||
expected_text = "You have deleted 1 layer from your project: %s" % \
|
||||
self.imported_layer_version.layer.name
|
||||
|
||||
self.assertTrue(expected_text in notification.text,
|
||||
"Expected notification text \"%s\" not found instead"
|
||||
"it was \"%s\"" %
|
||||
(expected_text, notification.text))
|
||||
|
||||
def test_addrm_to_project(self):
|
||||
self.get(self.url)
|
||||
|
||||
# Add the layer
|
||||
self.click("#add-remove-layer-btn")
|
||||
|
||||
notification = self.wait_until_visible("#change-notification-msg")
|
||||
|
||||
expected_text = "You have added 1 layer to your project: %s" % \
|
||||
self.imported_layer_version.layer.name
|
||||
|
||||
self.assertTrue(expected_text in notification.text,
|
||||
"Expected notification text %s not found was "
|
||||
" \"%s\" instead" %
|
||||
(expected_text, notification.text))
|
||||
|
||||
# Remove the layer
|
||||
self.click("#add-remove-layer-btn")
|
||||
|
||||
notification = self.wait_until_visible("#change-notification-msg")
|
||||
|
||||
expected_text = "You have removed 1 layer from your project: %s" % \
|
||||
self.imported_layer_version.layer.name
|
||||
|
||||
self.assertTrue(expected_text in notification.text,
|
||||
"Expected notification text %s not found was "
|
||||
" \"%s\" instead" %
|
||||
(expected_text, notification.text))
|
||||
@@ -0,0 +1,201 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
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 """
|
||||
|
||||
def _create_build_request(self):
|
||||
project = Project.objects.get_or_create_default_project()
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
build = Build.objects.create(project=project, build_name='fakebuild',
|
||||
started_on=now, completed_on=now)
|
||||
|
||||
return BuildRequest.objects.create(build=build, project=project,
|
||||
state=BuildRequest.REQ_QUEUED)
|
||||
|
||||
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='foo', layer_version=layer_version)
|
||||
|
||||
def _check_build_states(self, build_request):
|
||||
recipes_to_parse = 10
|
||||
url = reverse('all-builds')
|
||||
self.get(url)
|
||||
|
||||
build = build_request.build
|
||||
base_selector = '[data-latest-build-result="%s"] ' % build.id
|
||||
|
||||
# build queued; check shown as queued
|
||||
selector = base_selector + '[data-build-state="Queued"]'
|
||||
element = self.wait_until_visible(selector)
|
||||
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()
|
||||
|
||||
self.get(url)
|
||||
|
||||
selector = base_selector + '[data-build-state="Parsing"]'
|
||||
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)
|
||||
self.assertEqual(bar_element.value_of_css_property('width'), '0px',
|
||||
'recipe parse progress should be at 0')
|
||||
|
||||
# recipes being parsed; check parse progress
|
||||
build.recipes_parsed = 5
|
||||
build.save()
|
||||
|
||||
self.get(url)
|
||||
|
||||
element = self.wait_until_visible(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%'
|
||||
element = Wait(self.driver).until(recipe_bar_updated, msg)
|
||||
|
||||
# all recipes parsed, task started, waiting for first task to finish;
|
||||
# check status is shown as "Tasks starting..."
|
||||
build.recipes_parsed = recipes_to_parse
|
||||
build.save()
|
||||
|
||||
recipe = self._create_recipe()
|
||||
task1 = Task.objects.create(build=build, recipe=recipe,
|
||||
task_name='Lionel')
|
||||
task2 = Task.objects.create(build=build, recipe=recipe,
|
||||
task_name='Jeffries')
|
||||
|
||||
self.get(url)
|
||||
|
||||
selector = base_selector + '[data-build-state="Starting"]'
|
||||
element = self.wait_until_visible(selector)
|
||||
self.assertRegex(element.get_attribute('innerHTML'),
|
||||
'Tasks starting', 'build should show "tasks starting" status')
|
||||
|
||||
# first task finished; check tasks progress bar
|
||||
task1.outcome = Task.OUTCOME_SUCCESS
|
||||
task1.save()
|
||||
|
||||
self.get(url)
|
||||
|
||||
selector = base_selector + '[data-build-state="In Progress"]'
|
||||
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)
|
||||
|
||||
task_bar_updated = lambda driver: \
|
||||
bar_element.get_attribute('style') == 'width: 50%;'
|
||||
msg = 'tasks progress bar should update to 50%'
|
||||
element = Wait(self.driver).until(task_bar_updated, msg)
|
||||
|
||||
# last task finished; check tasks progress bar updates
|
||||
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)
|
||||
task_bar_updated = lambda driver: \
|
||||
bar_element.get_attribute('style') == 'width: 100%;'
|
||||
msg = 'tasks progress bar should update to 100%'
|
||||
element = Wait(self.driver).until(task_bar_updated, msg)
|
||||
|
||||
def test_states_to_success(self):
|
||||
"""
|
||||
Test state transitions in the recent builds area for a build which
|
||||
completes successfully.
|
||||
"""
|
||||
build_request = self._create_build_request()
|
||||
|
||||
self._check_build_states(build_request)
|
||||
|
||||
# all tasks complete and build succeeded; check success state shown
|
||||
build = build_request.build
|
||||
build.outcome = Build.SUCCEEDED
|
||||
build.save()
|
||||
|
||||
selector = '[data-latest-build-result="%s"] ' \
|
||||
'[data-build-state="Succeeded"]' % build.id
|
||||
element = self.wait_until_visible(selector)
|
||||
|
||||
def test_states_to_failure(self):
|
||||
"""
|
||||
Test state transitions in the recent builds area for a build which
|
||||
completes in a failure.
|
||||
"""
|
||||
build_request = self._create_build_request()
|
||||
|
||||
self._check_build_states(build_request)
|
||||
|
||||
# all tasks complete and build succeeded; check fail state shown
|
||||
build = build_request.build
|
||||
build.outcome = Build.FAILED
|
||||
build.save()
|
||||
|
||||
selector = '[data-latest-build-result="%s"] ' \
|
||||
'[data-build-state="Failed"]' % build.id
|
||||
element = self.wait_until_visible(selector)
|
||||
|
||||
def test_states_cancelling(self):
|
||||
"""
|
||||
Test that most recent build area updates correctly for a build
|
||||
which is cancelled.
|
||||
"""
|
||||
url = reverse('all-builds')
|
||||
|
||||
build_request = self._create_build_request()
|
||||
build = build_request.build
|
||||
|
||||
# cancel the build
|
||||
build_request.state = BuildRequest.REQ_CANCELLING
|
||||
build_request.save()
|
||||
|
||||
self.get(url)
|
||||
|
||||
# check cancelling state
|
||||
selector = '[data-latest-build-result="%s"] ' \
|
||||
'[data-build-state="Cancelling"]' % build.id
|
||||
element = self.wait_until_visible(selector)
|
||||
self.assertRegex(element.get_attribute('innerHTML'),
|
||||
'Cancelling the build', 'build should show "cancelling" status')
|
||||
|
||||
# check cancelled state
|
||||
build.outcome = Build.CANCELLED
|
||||
build.save()
|
||||
|
||||
self.get(url)
|
||||
|
||||
selector = '[data-latest-build-result="%s"] ' \
|
||||
'[data-build-state="Cancelled"]' % build.id
|
||||
element = self.wait_until_visible(selector)
|
||||
self.assertRegex(element.get_attribute('innerHTML'),
|
||||
'Build cancelled', 'build should show "cancelled" status')
|
||||
@@ -0,0 +1,159 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
from bldcontrol.models import BuildEnvironment
|
||||
|
||||
from django.urls import reverse
|
||||
from tests.browser.selenium_helpers import SeleniumTestCase
|
||||
|
||||
from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer
|
||||
from orm.models import Layer_Version, Recipe, CustomImageRecipe
|
||||
|
||||
|
||||
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')
|
||||
)
|
||||
|
||||
# project to add new custom images to
|
||||
self.project = Project.objects.create(name='foo', release=release)
|
||||
|
||||
# layer associated with the project
|
||||
layer = Layer.objects.create(name='bar')
|
||||
layer_version = Layer_Version.objects.create(
|
||||
layer=layer,
|
||||
project=self.project
|
||||
)
|
||||
|
||||
# properly add the layer to the project
|
||||
ProjectLayer.objects.create(
|
||||
project=self.project,
|
||||
layercommit=layer_version,
|
||||
optional=False
|
||||
)
|
||||
|
||||
# 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)
|
||||
layer_version2 = Layer_Version.objects.create(
|
||||
layer=layer,
|
||||
project=project2
|
||||
)
|
||||
ProjectLayer.objects.create(
|
||||
project=project2,
|
||||
layercommit=layer_version2,
|
||||
optional=False
|
||||
)
|
||||
recipe2 = Recipe.objects.create(
|
||||
name='core-image-minimal',
|
||||
layer_version=layer_version2,
|
||||
is_image=True
|
||||
)
|
||||
CustomImageRecipe.objects.create(
|
||||
name=self.CUSTOM_IMAGE_NAME,
|
||||
base_recipe=recipe2,
|
||||
layer_version=layer_version2,
|
||||
file_path='/1/2',
|
||||
project=project2
|
||||
)
|
||||
|
||||
def _create_custom_image(self, new_custom_image_name):
|
||||
"""
|
||||
1. Go to the 'new custom image' page
|
||||
2. Click the button for the fake core-image-minimal
|
||||
3. Wait for the dialog box for setting the name of the new custom
|
||||
image
|
||||
4. Insert new_custom_image_name into that dialog's text box
|
||||
"""
|
||||
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)
|
||||
|
||||
selector = '#new-custom-image-modal input[type="text"]'
|
||||
self.enter_text(selector, new_custom_image_name)
|
||||
|
||||
self.click('#create-new-custom-image-btn')
|
||||
|
||||
def _check_for_custom_image(self, image_name):
|
||||
"""
|
||||
Fetch the list of custom images for the project and check the
|
||||
image with name image_name is listed there
|
||||
"""
|
||||
url = reverse('projectcustomimages', args=(self.project.id,))
|
||||
self.get(url)
|
||||
|
||||
self.wait_until_visible('#customimagestable')
|
||||
|
||||
element = self.find('#customimagestable td[class="name"] a')
|
||||
msg = 'should be a custom image link with text %s' % image_name
|
||||
self.assertEqual(element.text.strip(), image_name, msg)
|
||||
|
||||
def test_new_image(self):
|
||||
"""
|
||||
Should be able to create a new custom image
|
||||
"""
|
||||
custom_image_name = 'boo-image'
|
||||
self._create_custom_image(custom_image_name)
|
||||
self.wait_until_visible('#image-created-notification')
|
||||
self._check_for_custom_image(custom_image_name)
|
||||
|
||||
def test_new_duplicates_other_project_image(self):
|
||||
"""
|
||||
Should be able to create a new custom image if its name is the same
|
||||
as a custom image in another project
|
||||
"""
|
||||
self._create_custom_image(self.CUSTOM_IMAGE_NAME)
|
||||
self.wait_until_visible('#image-created-notification')
|
||||
self._check_for_custom_image(self.CUSTOM_IMAGE_NAME)
|
||||
|
||||
def test_new_duplicates_non_image_recipe(self):
|
||||
"""
|
||||
Should not be able to create a new custom image whose name is the
|
||||
same as an existing non-image recipe
|
||||
"""
|
||||
self._create_custom_image(self.recipe.name)
|
||||
element = self.wait_until_visible('#invalid-name-help')
|
||||
self.assertRegex(element.text.strip(),
|
||||
'image with this name already exists')
|
||||
|
||||
def test_new_duplicates_project_image(self):
|
||||
"""
|
||||
Should not be able to create a new custom image whose name is the same
|
||||
as a custom image in this project
|
||||
"""
|
||||
# create the image
|
||||
custom_image_name = 'doh-image'
|
||||
self._create_custom_image(custom_image_name)
|
||||
self.wait_until_visible('#image-created-notification')
|
||||
self._check_for_custom_image(custom_image_name)
|
||||
|
||||
# try to create an image with the same name
|
||||
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.assertRegex(element.text.strip(), expected)
|
||||
@@ -0,0 +1,109 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
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
|
||||
|
||||
|
||||
class TestNewProjectPage(SeleniumTestCase):
|
||||
""" Test project data at /project/X/ is displayed correctly """
|
||||
|
||||
def setUp(self):
|
||||
bitbake, c = BitbakeVersion.objects.get_or_create(
|
||||
name="master",
|
||||
giturl="git://master",
|
||||
branch="master",
|
||||
dirpath="master")
|
||||
|
||||
release, c = Release.objects.get_or_create(name="msater",
|
||||
description="master"
|
||||
"release",
|
||||
branch_name="master",
|
||||
helptext="latest",
|
||||
bitbake_version=bitbake)
|
||||
|
||||
self.release, c = Release.objects.get_or_create(
|
||||
name="msater2",
|
||||
description="master2"
|
||||
"release2",
|
||||
branch_name="master2",
|
||||
helptext="latest2",
|
||||
bitbake_version=bitbake)
|
||||
|
||||
def test_create_new_project(self):
|
||||
""" Test creating a project """
|
||||
|
||||
project_name = "masterproject"
|
||||
|
||||
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))
|
||||
|
||||
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', poll=3)
|
||||
|
||||
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")
|
||||
|
||||
def test_new_duplicates_project_name(self):
|
||||
"""
|
||||
Should not be able to create a new project whose name is the same
|
||||
as an existing project
|
||||
"""
|
||||
|
||||
project_name = "dupproject"
|
||||
|
||||
Project.objects.create_project(name=project_name,
|
||||
release=self.release)
|
||||
|
||||
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))
|
||||
|
||||
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")
|
||||
|
||||
# Try and click it anyway, if it submits we'll have a new project in
|
||||
# the db and assert then
|
||||
try:
|
||||
self.click("#create-project-button")
|
||||
except InvalidElementStateException:
|
||||
pass
|
||||
|
||||
self.assertTrue(
|
||||
(Project.objects.filter(name=project_name).count() == 1),
|
||||
"New project not found in database")
|
||||
@@ -0,0 +1,158 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
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, Target
|
||||
|
||||
class TestProjectBuildsPage(SeleniumTestCase):
|
||||
""" Test data at /project/X/builds is displayed correctly """
|
||||
|
||||
PROJECT_NAME = 'test project'
|
||||
CLI_BUILDS_PROJECT_NAME = 'command line builds'
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
|
||||
release=release)
|
||||
self.project1.save()
|
||||
|
||||
self.project2 = Project.objects.create_project(name=self.PROJECT_NAME,
|
||||
release=release)
|
||||
self.project2.save()
|
||||
|
||||
self.default_project = Project.objects.create_project(
|
||||
name=self.CLI_BUILDS_PROJECT_NAME,
|
||||
release=release
|
||||
)
|
||||
self.default_project.is_default = True
|
||||
self.default_project.save()
|
||||
|
||||
# parameters for builds to associate with the projects
|
||||
now = timezone.now()
|
||||
|
||||
self.project1_build_success = {
|
||||
'project': self.project1,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.SUCCEEDED
|
||||
}
|
||||
|
||||
self.project1_build_in_progress = {
|
||||
'project': self.project1,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.IN_PROGRESS
|
||||
}
|
||||
|
||||
self.project2_build_success = {
|
||||
'project': self.project2,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.SUCCEEDED
|
||||
}
|
||||
|
||||
self.project2_build_in_progress = {
|
||||
'project': self.project2,
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.IN_PROGRESS
|
||||
}
|
||||
|
||||
def _get_rows_for_project(self, project_id):
|
||||
"""
|
||||
Helper to retrieve HTML rows for a project's builds,
|
||||
as shown in the main table of the page
|
||||
"""
|
||||
url = reverse('projectbuilds', args=(project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_present('#projectbuildstable tbody tr')
|
||||
return self.find_all('#projectbuildstable tbody tr')
|
||||
|
||||
def test_show_builds_for_project(self):
|
||||
""" Builds for a project should be displayed in the main table """
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
build_rows = self._get_rows_for_project(self.project1.id)
|
||||
self.assertEqual(len(build_rows), 2)
|
||||
|
||||
def test_show_builds_project_only(self):
|
||||
""" Builds for other projects should be excluded """
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
|
||||
# shouldn't see these two
|
||||
Build.objects.create(**self.project2_build_success)
|
||||
Build.objects.create(**self.project2_build_in_progress)
|
||||
|
||||
build_rows = self._get_rows_for_project(self.project1.id)
|
||||
self.assertEqual(len(build_rows), 3)
|
||||
|
||||
def test_builds_exclude_in_progress(self):
|
||||
""" "in progress" builds should not be shown in main table """
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
Build.objects.create(**self.project1_build_success)
|
||||
|
||||
# shouldn't see this one
|
||||
Build.objects.create(**self.project1_build_in_progress)
|
||||
|
||||
# shouldn't see these two either, as they belong to a different project
|
||||
Build.objects.create(**self.project2_build_success)
|
||||
Build.objects.create(**self.project2_build_in_progress)
|
||||
|
||||
build_rows = self._get_rows_for_project(self.project1.id)
|
||||
self.assertEqual(len(build_rows), 2)
|
||||
|
||||
def test_show_tasks_with_suffix(self):
|
||||
""" Task should be shown as suffixes on build names """
|
||||
build = Build.objects.create(**self.project1_build_success)
|
||||
target = 'bash'
|
||||
task = 'clean'
|
||||
Target.objects.create(build=build, target=target, task=task)
|
||||
|
||||
url = reverse('projectbuilds', args=(self.project1.id,))
|
||||
self.get(url)
|
||||
self.wait_until_present('td[class="target"]')
|
||||
|
||||
cell = self.find('td[class="target"]')
|
||||
content = cell.get_attribute('innerHTML')
|
||||
expected_text = '%s:%s' % (target, task)
|
||||
|
||||
self.assertTrue(re.search(expected_text, content),
|
||||
'"target" cell should contain text %s' % expected_text)
|
||||
|
||||
def test_cli_builds_hides_tabs(self):
|
||||
"""
|
||||
Display for command line builds should hide tabs
|
||||
"""
|
||||
url = reverse('projectbuilds', args=(self.default_project.id,))
|
||||
self.get(url)
|
||||
tabs = self.find_all('#project-topbar')
|
||||
self.assertEqual(len(tabs), 0,
|
||||
'should be no top bar shown for command line builds')
|
||||
|
||||
def test_non_cli_builds_has_tabs(self):
|
||||
"""
|
||||
Non-command-line builds projects should show the tabs
|
||||
"""
|
||||
url = reverse('projectbuilds', args=(self.project1.id,))
|
||||
self.get(url)
|
||||
tabs = self.find_all('#project-topbar')
|
||||
self.assertEqual(len(tabs), 1,
|
||||
'should be a top bar shown for non-command-line builds')
|
||||
@@ -0,0 +1,220 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
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 """
|
||||
|
||||
PROJECT_NAME = 'test project'
|
||||
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 setUp(self):
|
||||
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)
|
||||
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
|
||||
release=release)
|
||||
self.project1.save()
|
||||
|
||||
|
||||
def test_no_underscore_iamgefs_type(self):
|
||||
"""
|
||||
Should not accept IMAGEFS_TYPE with an underscore
|
||||
"""
|
||||
|
||||
imagefs_type = "foo_bar"
|
||||
|
||||
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
|
||||
url = reverse('projectconf', args=(self.project1.id,));
|
||||
self.get(url);
|
||||
|
||||
self.click('#change-image_fstypes-icon')
|
||||
|
||||
self.enter_text('#new-imagefs_types', imagefs_type)
|
||||
|
||||
element = self.wait_until_visible('#hintError-image-fs_type')
|
||||
|
||||
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.
|
||||
"""
|
||||
imagefs_type = "btrfs"
|
||||
|
||||
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
|
||||
url = reverse('projectconf', args=(self.project1.id,));
|
||||
self.get(url);
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
|
||||
url = reverse('projectconf', args=(self.project1.id,));
|
||||
self.get(url);
|
||||
|
||||
self.click('#change-image_fstypes-icon')
|
||||
|
||||
self.wait_until_visible('#new-imagefs_types')
|
||||
|
||||
checkboxes_selector = '.fs-checkbox-fstypes'
|
||||
|
||||
self.wait_until_visible(checkboxes_selector)
|
||||
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')
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
ProjectVariable.objects.get_or_create(project=self.project1,
|
||||
name='DL_DIR')
|
||||
url = reverse('projectconf', args=(self.project1.id,))
|
||||
self.get(url)
|
||||
|
||||
# activate the input to edit download dir
|
||||
self.click('#change-dl_dir-icon')
|
||||
self.wait_until_visible('#new-dl_dir')
|
||||
|
||||
# downloads dir path doesn't start with / or ${...}
|
||||
self.enter_text('#new-dl_dir', 'home/foo')
|
||||
element = self.wait_until_visible('#hintError-initialChar-dl_dir')
|
||||
|
||||
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')
|
||||
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')
|
||||
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
|
||||
"""
|
||||
|
||||
ProjectVariable.objects.get_or_create(project=self.project1,
|
||||
name='SSTATE_DIR')
|
||||
url = reverse('projectconf', args=(self.project1.id,))
|
||||
self.get(url)
|
||||
|
||||
self.click('#change-sstate_dir-icon')
|
||||
|
||||
self.wait_until_visible('#new-sstate_dir')
|
||||
|
||||
# path doesn't start with / or ${...}
|
||||
self.enter_text('#new-sstate_dir', 'home/foo')
|
||||
element = self.wait_until_visible('#hintError-initialChar-sstate_dir')
|
||||
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from tests.browser.selenium_helpers import SeleniumTestCase
|
||||
|
||||
from orm.models import Build, Project
|
||||
|
||||
class TestProjectPage(SeleniumTestCase):
|
||||
""" Test project data at /project/X/ is displayed correctly """
|
||||
|
||||
CLI_BUILDS_PROJECT_NAME = 'Command line builds'
|
||||
|
||||
def test_cli_builds_in_progress(self):
|
||||
"""
|
||||
In progress builds should not cause an error to be thrown
|
||||
when navigating to "command line builds" project page;
|
||||
see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
|
||||
"""
|
||||
|
||||
# add the "command line builds" default project; this mirrors what
|
||||
# we do with get_or_create_default_project()
|
||||
default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
|
||||
default_project.is_default = True
|
||||
default_project.save()
|
||||
|
||||
# add an "in progress" build for the default project
|
||||
now = timezone.now()
|
||||
Build.objects.create(project=default_project,
|
||||
started_on=now,
|
||||
completed_on=now,
|
||||
outcome=Build.IN_PROGRESS)
|
||||
|
||||
# navigate to the project page for the default project
|
||||
url = reverse("project", args=(default_project.id,))
|
||||
self.get(url)
|
||||
|
||||
# check that we get a project page with the correct heading
|
||||
project_name = self.find('.project-name').text.strip()
|
||||
self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME)
|
||||
@@ -0,0 +1,39 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
"""
|
||||
A small example test demonstrating the basics of writing a test with
|
||||
Toaster's SeleniumTestCase; this just fetches the Toaster home page
|
||||
and checks it has the word "Toaster" in the brand link
|
||||
|
||||
New test files should follow this structure, should be named "test_*.py",
|
||||
and should be in the same directory as this sample.
|
||||
"""
|
||||
|
||||
from django.urls import reverse
|
||||
from tests.browser.selenium_helpers import SeleniumTestCase
|
||||
|
||||
class TestSample(SeleniumTestCase):
|
||||
""" Test landing page shows the Toaster brand """
|
||||
|
||||
def test_landing_page_has_brand(self):
|
||||
url = reverse('landing')
|
||||
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)
|
||||
@@ -0,0 +1,64 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
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
|
||||
from orm.models import Task, Task_Dependency
|
||||
|
||||
class TestTaskPage(SeleniumTestCase):
|
||||
""" Test page which shows an individual task """
|
||||
RECIPE_NAME = 'bar'
|
||||
RECIPE_VERSION = '0.1'
|
||||
TASK_NAME = 'do_da_doo_ron_ron'
|
||||
|
||||
def setUp(self):
|
||||
now = timezone.now()
|
||||
|
||||
project = Project.objects.get_or_create_default_project()
|
||||
|
||||
self.build = Build.objects.create(project=project, started_on=now,
|
||||
completed_on=now)
|
||||
|
||||
Target.objects.create(target='foo', build=self.build)
|
||||
|
||||
layer = Layer.objects.create()
|
||||
|
||||
layer_version = Layer_Version.objects.create(layer=layer)
|
||||
|
||||
recipe = Recipe.objects.create(name=TestTaskPage.RECIPE_NAME,
|
||||
layer_version=layer_version, version=TestTaskPage.RECIPE_VERSION)
|
||||
|
||||
self.task = Task.objects.create(build=self.build, recipe=recipe,
|
||||
order=1, outcome=Task.OUTCOME_COVERED, task_executed=False,
|
||||
task_name=TestTaskPage.TASK_NAME)
|
||||
|
||||
def test_covered_task(self):
|
||||
"""
|
||||
Check that covered tasks are displayed for tasks which have
|
||||
dependencies on themselves
|
||||
"""
|
||||
|
||||
# the infinite loop which of bug 9952 was down to tasks which
|
||||
# depend on themselves, so add self-dependent tasks to replicate the
|
||||
# situation which caused the infinite loop (now fixed)
|
||||
Task_Dependency.objects.create(task=self.task, depends_on=self.task)
|
||||
|
||||
url = reverse('task', args=(self.build.id, self.task.id,))
|
||||
self.get(url)
|
||||
|
||||
# check that we see the task name
|
||||
self.wait_until_visible('.page-header h1')
|
||||
|
||||
heading = self.find('.page-header h1')
|
||||
expected_heading = '%s_%s %s' % (TestTaskPage.RECIPE_NAME,
|
||||
TestTaskPage.RECIPE_VERSION, TestTaskPage.TASK_NAME)
|
||||
self.assertEqual(heading.text, expected_heading,
|
||||
'Heading should show recipe name, version and task')
|
||||
@@ -0,0 +1,151 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2013-2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
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):
|
||||
"""
|
||||
Tests for the UI elements of ToasterTable (sorting etc.);
|
||||
note that the tests cover generic functionality of ToasterTable which
|
||||
manifests as UI elements in the browser, and can only be tested via
|
||||
Selenium.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def _get_orderby_heading(self, table):
|
||||
"""
|
||||
Get the current order by finding the column heading in <table> with
|
||||
the sorted class on it.
|
||||
|
||||
table: WebElement for a ToasterTable
|
||||
"""
|
||||
selector = 'thead a.sorted'
|
||||
heading = table.find_element(By.CSS_SELECTOR, selector)
|
||||
return heading.get_attribute('innerHTML').strip()
|
||||
|
||||
def _get_datetime_from_cell(self, row, selector):
|
||||
"""
|
||||
Return the value in the cell selected by <selector> on <row> as a
|
||||
datetime.
|
||||
|
||||
row: <tr> WebElement for a row in the ToasterTable
|
||||
selector: CSS selector to use to find the cell containing the date time
|
||||
string
|
||||
"""
|
||||
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')
|
||||
|
||||
def test_revert_orderby(self):
|
||||
"""
|
||||
Test that sort order for a table reverts to the default sort order
|
||||
if the current sort column is hidden.
|
||||
"""
|
||||
now = timezone.now()
|
||||
later = now + timezone.timedelta(hours=1)
|
||||
even_later = later + timezone.timedelta(hours=1)
|
||||
|
||||
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',
|
||||
bitbake_version=bbv)
|
||||
|
||||
project = Project.objects.create_project('project', release)
|
||||
|
||||
# set up two builds which will order differently when sorted by
|
||||
# started_on or completed_on
|
||||
|
||||
# started first, finished last
|
||||
build1 = Build.objects.create(project=project,
|
||||
started_on=now,
|
||||
completed_on=even_later,
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
# started second, finished first
|
||||
build2 = Build.objects.create(project=project,
|
||||
started_on=later,
|
||||
completed_on=later,
|
||||
outcome=Build.SUCCEEDED)
|
||||
|
||||
url = reverse('all-builds')
|
||||
self.get(url)
|
||||
table = self.wait_until_visible('#allbuildstable')
|
||||
|
||||
# check ordering (default is by -completed_on); so build1 should be
|
||||
# first as it finished last
|
||||
active_heading = self._get_orderby_heading(table)
|
||||
self.assertEqual(active_heading, 'Completed on',
|
||||
'table should be sorted by "Completed on" by default')
|
||||
|
||||
row_selector = '#allbuildstable tbody tr'
|
||||
cell_selector = 'td.completed_on'
|
||||
|
||||
rows = self.find_all(row_selector)
|
||||
row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector)
|
||||
row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector)
|
||||
self.assertTrue(row1_completed_on > row2_completed_on,
|
||||
'table should be sorted by -completed_on')
|
||||
|
||||
# turn on started_on column
|
||||
self.click('#edit-columns-button')
|
||||
self.click('#checkbox-started_on')
|
||||
|
||||
# sort by started_on column
|
||||
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()
|
||||
break
|
||||
|
||||
# wait for table data to reload in response to new sort
|
||||
self.wait_until_visible('#allbuildstable')
|
||||
|
||||
# check ordering; build1 should be first
|
||||
active_heading = self._get_orderby_heading(table)
|
||||
self.assertEqual(active_heading, 'Started on',
|
||||
'table should be sorted by "Started on"')
|
||||
|
||||
cell_selector = 'td.started_on'
|
||||
|
||||
rows = self.find_all(row_selector)
|
||||
row1_started_on = self._get_datetime_from_cell(rows[0], cell_selector)
|
||||
row2_started_on = self._get_datetime_from_cell(rows[1], cell_selector)
|
||||
self.assertTrue(row1_started_on < row2_started_on,
|
||||
'table should be sorted by started_on')
|
||||
|
||||
# turn off started_on column
|
||||
self.click('#edit-columns-button')
|
||||
self.click('#checkbox-started_on')
|
||||
|
||||
# wait for table data to reload in response to new sort
|
||||
self.wait_until_visible('#allbuildstable')
|
||||
|
||||
# check ordering (should revert to completed_on); build2 should be first
|
||||
active_heading = self._get_orderby_heading(table)
|
||||
self.assertEqual(active_heading, 'Completed on',
|
||||
'table should be sorted by "Completed on" after hiding sort column')
|
||||
|
||||
cell_selector = 'td.completed_on'
|
||||
|
||||
rows = self.find_all(row_selector)
|
||||
row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector)
|
||||
row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector)
|
||||
self.assertTrue(row1_completed_on > row2_completed_on,
|
||||
'table should be sorted by -completed_on')
|
||||
14
sources/poky/bitbake/lib/toaster/tests/builds/README
Normal file
14
sources/poky/bitbake/lib/toaster/tests/builds/README
Normal file
@@ -0,0 +1,14 @@
|
||||
# Running build tests
|
||||
|
||||
These tests are to test the running of builds and the data produced by the builds.
|
||||
Your oe build environment must be sourced/initialised for these tests to run.
|
||||
|
||||
The simplest way to run the tests are the following commands:
|
||||
|
||||
$ . oe-init-build-env
|
||||
$ cd bitbake/lib/toaster/ # path my vary but this is into toaster's directory
|
||||
$ DJANGO_SETTINGS_MODULE='toastermain.settings_test' ./manage.py test tests.builds
|
||||
|
||||
Optional environment variables:
|
||||
- TOASTER_DIR (where toaster keeps it's artifacts)
|
||||
- TOASTER_CONF a path to the toasterconf.json file. This will need to be set if you don't execute the tests from toaster's own directory.
|
||||
166
sources/poky/bitbake/lib/toaster/tests/builds/buildtest.py
Normal file
166
sources/poky/bitbake/lib/toaster/tests/builds/buildtest.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable
|
||||
from bldcontrol.models import BuildEnvironment
|
||||
|
||||
from bldcontrol.management.commands.runbuilds import Command\
|
||||
as RunBuildsCommand
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("toaster")
|
||||
|
||||
# We use unittest.TestCase instead of django.test.TestCase because we don't
|
||||
# want to wrap everything in a database transaction as an external process
|
||||
# (bitbake needs access to the database)
|
||||
|
||||
def load_build_environment():
|
||||
call_command('loaddata', 'settings.xml', app_label="orm")
|
||||
call_command('loaddata', 'poky.xml', app_label="orm")
|
||||
|
||||
current_builddir = os.environ.get("BUILDDIR")
|
||||
if current_builddir:
|
||||
BuildTest.BUILDDIR = current_builddir
|
||||
else:
|
||||
# Setup a builddir based on default layout
|
||||
# bitbake inside openebedded-core
|
||||
oe_init_build_env_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
'oe-init-build-env'
|
||||
)
|
||||
if not os.path.exists(oe_init_build_env_path):
|
||||
raise Exception("We had no BUILDDIR set and couldn't "
|
||||
"find oe-init-build-env to set this up "
|
||||
"ourselves please run oe-init-build-env "
|
||||
"before running these tests")
|
||||
|
||||
oe_init_build_env_path = os.path.realpath(oe_init_build_env_path)
|
||||
cmd = "bash -c 'source oe-init-build-env %s'" % BuildTest.BUILDDIR
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.path.dirname(oe_init_build_env_path),
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
output, err = p.communicate()
|
||||
p.wait()
|
||||
|
||||
logger.info("oe-init-build-env %s %s" % (output, err))
|
||||
|
||||
os.environ['BUILDDIR'] = BuildTest.BUILDDIR
|
||||
|
||||
# Setup the path to bitbake we know where to find this
|
||||
bitbake_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
'bin',
|
||||
'bitbake')
|
||||
if not os.path.exists(bitbake_path):
|
||||
raise Exception("Could not find bitbake at the expected path %s"
|
||||
% bitbake_path)
|
||||
|
||||
os.environ['BBBASEDIR'] = bitbake_path
|
||||
|
||||
class BuildTest(unittest.TestCase):
|
||||
|
||||
PROJECT_NAME = "Testbuild"
|
||||
BUILDDIR = os.environ.get("BUILDDIR")
|
||||
|
||||
def build(self, target):
|
||||
# So that the buildinfo helper uses the test database'
|
||||
self.assertEqual(
|
||||
os.environ.get('DJANGO_SETTINGS_MODULE', ''),
|
||||
'toastermain.settings_test',
|
||||
"Please initialise django with the tests settings: "
|
||||
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
|
||||
|
||||
built = self.target_already_built(target)
|
||||
if built:
|
||||
return built
|
||||
|
||||
load_build_environment()
|
||||
|
||||
BuildEnvironment.objects.get_or_create(
|
||||
betype=BuildEnvironment.TYPE_LOCAL,
|
||||
sourcedir=BuildTest.BUILDDIR,
|
||||
builddir=BuildTest.BUILDDIR
|
||||
)
|
||||
|
||||
release = Release.objects.get(name='local')
|
||||
|
||||
# Create a project for this build to run in
|
||||
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://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH",
|
||||
project=project)
|
||||
|
||||
ProjectTarget.objects.create(project=project,
|
||||
target=target,
|
||||
task="")
|
||||
build_request = project.schedule_build()
|
||||
|
||||
# run runbuilds command to dispatch the build
|
||||
# e.g. manage.py runubilds
|
||||
RunBuildsCommand().runbuild()
|
||||
|
||||
build_pk = build_request.build.pk
|
||||
while Build.objects.get(pk=build_pk).outcome == Build.IN_PROGRESS:
|
||||
sys.stdout.write("\rBuilding %s %d%%" %
|
||||
(target,
|
||||
build_request.build.completeper()))
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
|
||||
self.assertEqual(Build.objects.get(pk=build_pk).outcome,
|
||||
Build.SUCCEEDED,
|
||||
"Build did not SUCCEEDED")
|
||||
|
||||
logger.info("\nBuild finished %s" % build_request.build.outcome)
|
||||
return build_request.build
|
||||
|
||||
def target_already_built(self, target):
|
||||
""" If the target is already built no need to build it again"""
|
||||
for build in Build.objects.filter(
|
||||
project__name=BuildTest.PROJECT_NAME):
|
||||
targets = build.target_set.values_list('target', flat=True)
|
||||
if target in targets:
|
||||
return build
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,363 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
|
||||
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from orm.models import Target_Image_File, Target_Installed_Package, Task
|
||||
from orm.models import Package_Dependency, Recipe_Dependency, Build
|
||||
from orm.models import Task_Dependency, Package, Target, Recipe
|
||||
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.target_already_built("core-image-minimal")
|
||||
|
||||
# Check if build name is unique - tc_id=795
|
||||
def test_Build_Unique_Name(self):
|
||||
all_builds = Build.objects.all().count()
|
||||
distinct_builds = Build.objects.values('id').distinct().count()
|
||||
self.assertEqual(distinct_builds,
|
||||
all_builds,
|
||||
msg='Build name is not unique')
|
||||
|
||||
# Check if build cooker log path is unique - tc_id=819
|
||||
def test_Build_Unique_Cooker_Log_Path(self):
|
||||
distinct_path = Build.objects.values(
|
||||
'cooker_log_path').distinct().count()
|
||||
total_builds = Build.objects.values('id').count()
|
||||
self.assertEqual(distinct_path,
|
||||
total_builds,
|
||||
msg='Build cooker log path is not unique')
|
||||
|
||||
# Check task order sequence for one build - tc=825
|
||||
def test_Task_Order_Sequence(self):
|
||||
cnt_err = []
|
||||
tasks = Task.objects.filter(
|
||||
Q(build=self.completed_build),
|
||||
~Q(order=None),
|
||||
~Q(task_name__contains='_setscene')
|
||||
).values('id', 'order').order_by("order")
|
||||
|
||||
cnt_tasks = 0
|
||||
for task in tasks:
|
||||
cnt_tasks += 1
|
||||
if (task['order'] != cnt_tasks):
|
||||
cnt_err.append(task['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if disk_io matches the difference between EndTimeIO and
|
||||
# StartTimeIO in build stats - tc=828
|
||||
# def test_Task_Disk_IO_TC828(self):
|
||||
|
||||
# Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) -
|
||||
# tc=832
|
||||
def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self):
|
||||
tasks = Task.objects.filter(outcome=2).values('id', 'sstate_result')
|
||||
cnt_err = []
|
||||
for task in tasks:
|
||||
if (task['sstate_result'] != 3):
|
||||
cnt_err.append(task['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must
|
||||
# be 0 (SSTATE_NA) - tc=833
|
||||
def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self):
|
||||
tasks = Task.objects.filter(
|
||||
outcome__in=(Task.OUTCOME_COVERED,
|
||||
Task.OUTCOME_PREBUILT)).values('id',
|
||||
'task_name',
|
||||
'sstate_result')
|
||||
cnt_err = []
|
||||
for task in tasks:
|
||||
if (task['sstate_result'] != Task.SSTATE_NA and
|
||||
task['sstate_result'] != Task.SSTATE_MISS):
|
||||
cnt_err.append({'id': task['id'],
|
||||
'name': task['task_name'],
|
||||
'sstate_result': task['sstate_result']})
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be
|
||||
# 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834
|
||||
def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self):
|
||||
tasks = Task.objects.filter(
|
||||
outcome__in=(0, 4)).values('id', 'sstate_result')
|
||||
cnt_err = []
|
||||
|
||||
for task in tasks:
|
||||
if (task['sstate_result'] not in [0, 1, 2]):
|
||||
cnt_err.append(task['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2
|
||||
# (CODING_PYTHON), 3 (CODING_SHELL) - tc=891
|
||||
def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self):
|
||||
tasks = Task.objects.filter(
|
||||
task_executed=1).values('id', 'script_type')
|
||||
cnt_err = []
|
||||
|
||||
for task in tasks:
|
||||
if (task['script_type'] not in [0, 2, 3]):
|
||||
cnt_err.append(task['id'])
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4
|
||||
# (FAILED) - tc=836
|
||||
def test_Task_If_Task_Executed_True_Outcome_0_4(self):
|
||||
tasks = Task.objects.filter(task_executed=1).values('id', 'outcome')
|
||||
cnt_err = []
|
||||
|
||||
for task in tasks:
|
||||
if (task['outcome'] not in [0, 4]):
|
||||
cnt_err.append(task['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if task_executed = FALSE (0), script_type must be 0 - tc=890
|
||||
def test_Task_If_Task_Executed_False_Script_Type_0(self):
|
||||
tasks = Task.objects.filter(
|
||||
task_executed=0).values('id', 'script_type')
|
||||
cnt_err = []
|
||||
|
||||
for task in tasks:
|
||||
if (task['script_type'] != 0):
|
||||
cnt_err.append(task['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0),
|
||||
# task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) -
|
||||
# tc=837
|
||||
def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self):
|
||||
builds = Build.objects.filter(outcome=0).values('id')
|
||||
cnt_err = []
|
||||
for build in builds:
|
||||
tasks = Task.objects.filter(
|
||||
build=build['id'], task_executed=0).values('id', 'outcome')
|
||||
for task in tasks:
|
||||
if (task['outcome'] not in [1, 2, 3, 5]):
|
||||
cnt_err.append(task['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task id: %s' % cnt_err)
|
||||
|
||||
# Key verification - tc=888
|
||||
def test_Target_Installed_Package(self):
|
||||
rows = Target_Installed_Package.objects.values('id',
|
||||
'target_id',
|
||||
'package_id')
|
||||
cnt_err = []
|
||||
|
||||
for row in rows:
|
||||
target = Target.objects.filter(id=row['target_id']).values('id')
|
||||
package = Package.objects.filter(id=row['package_id']).values('id')
|
||||
if (not target or not package):
|
||||
cnt_err.append(row['id'])
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for target installed package id: %s' %
|
||||
cnt_err)
|
||||
|
||||
# Key verification - tc=889
|
||||
def test_Task_Dependency(self):
|
||||
rows = Task_Dependency.objects.values('id',
|
||||
'task_id',
|
||||
'depends_on_id')
|
||||
cnt_err = []
|
||||
for row in rows:
|
||||
task_id = Task.objects.filter(id=row['task_id']).values('id')
|
||||
depends_on_id = Task.objects.filter(
|
||||
id=row['depends_on_id']).values('id')
|
||||
if (not task_id or not depends_on_id):
|
||||
cnt_err.append(row['id'])
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg='Errors for task dependency id: %s' % cnt_err)
|
||||
|
||||
# Check if build target file_name is populated only if is_image=true AND
|
||||
# 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(
|
||||
build_id=build['id'], is_image=1).values('id')
|
||||
for target in targets:
|
||||
target_files = Target_Image_File.objects.filter(
|
||||
target_id=target['id']).values('id',
|
||||
'file_name',
|
||||
'file_size')
|
||||
for file_info in target_files:
|
||||
target_id = file_info['id']
|
||||
target_file_name = file_info['file_name']
|
||||
target_file_size = file_info['file_size']
|
||||
if (not target_file_name or not target_file_size):
|
||||
cnt_err.append(target_id)
|
||||
else:
|
||||
if (not os.path.exists(target_file_name)):
|
||||
cnt_err.append(target_id)
|
||||
else:
|
||||
if (os.path.getsize(target_file_name) !=
|
||||
target_file_size):
|
||||
cnt_err.append(target_id)
|
||||
self.assertEqual(len(cnt_err), 0,
|
||||
msg='Errors for target image file id: %s' %
|
||||
cnt_err)
|
||||
|
||||
# Key verification - tc=884
|
||||
def test_Package_Dependency(self):
|
||||
cnt_err = []
|
||||
deps = Package_Dependency.objects.values(
|
||||
'id', 'package_id', 'depends_on_id')
|
||||
for dep in deps:
|
||||
if (dep['package_id'] == dep['depends_on_id']):
|
||||
cnt_err.append(dep['id'])
|
||||
self.assertEqual(len(cnt_err), 0,
|
||||
msg='Errors for package dependency id: %s' % cnt_err)
|
||||
|
||||
# Recipe key verification, recipe name does not depends on a recipe having
|
||||
# the same name - tc=883
|
||||
def test_Recipe_Dependency(self):
|
||||
deps = Recipe_Dependency.objects.values(
|
||||
'id', 'recipe_id', 'depends_on_id')
|
||||
cnt_err = []
|
||||
for dep in deps:
|
||||
if (not dep['recipe_id'] or not dep['depends_on_id']):
|
||||
cnt_err.append(dep['id'])
|
||||
else:
|
||||
name = Recipe.objects.filter(
|
||||
id=dep['recipe_id']).values('name')
|
||||
dep_name = Recipe.objects.filter(
|
||||
id=dep['depends_on_id']).values('name')
|
||||
if (name == dep_name):
|
||||
cnt_err.append(dep['id'])
|
||||
self.assertEqual(len(cnt_err), 0,
|
||||
msg='Errors for recipe dependency id: %s' % cnt_err)
|
||||
|
||||
# Check if package name does not start with a number (0-9) - tc=846
|
||||
def test_Package_Name_For_Number(self):
|
||||
packages = Package.objects.filter(~Q(size=-1)).values('id', 'name')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
if (package['name'][0].isdigit() is True):
|
||||
cnt_err.append(package['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
|
||||
|
||||
# Check if package version starts with a number (0-9) - tc=847
|
||||
def test_Package_Version_Starts_With_Number(self):
|
||||
packages = Package.objects.filter(
|
||||
~Q(size=-1)).values('id', 'version')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
if (package['version'][0].isdigit() is False):
|
||||
cnt_err.append(package['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
|
||||
|
||||
# Check if package revision starts with 'r' - tc=848
|
||||
def test_Package_Revision_Starts_With_r(self):
|
||||
packages = Package.objects.filter(
|
||||
~Q(size=-1)).values('id', 'revision')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
if (package['revision'][0].startswith("r") is False):
|
||||
cnt_err.append(package['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
|
||||
|
||||
# Check the validity of the package build_id
|
||||
# TC must be added in test run
|
||||
def test_Package_Build_Id(self):
|
||||
packages = Package.objects.filter(
|
||||
~Q(size=-1)).values('id', 'build_id')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
build_id = Build.objects.filter(
|
||||
id=package['build_id']).values('id')
|
||||
if (not build_id):
|
||||
# They have no build_id but if they are
|
||||
# CustomImagePackage that's expected
|
||||
try:
|
||||
CustomImagePackage.objects.get(pk=package['id'])
|
||||
except CustomImagePackage.DoesNotExist:
|
||||
cnt_err.append(package['id'])
|
||||
|
||||
self.assertEqual(len(cnt_err),
|
||||
0,
|
||||
msg="Errors for package id: %s they have no build"
|
||||
"associated with them" % cnt_err)
|
||||
|
||||
# Check the validity of package recipe_id
|
||||
# TC must be added in test run
|
||||
def test_Package_Recipe_Id(self):
|
||||
packages = Package.objects.filter(
|
||||
~Q(size=-1)).values('id', 'recipe_id')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
recipe_id = Recipe.objects.filter(
|
||||
id=package['recipe_id']).values('id')
|
||||
if (not recipe_id):
|
||||
cnt_err.append(package['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
|
||||
|
||||
# Check if package installed_size field is not null
|
||||
# TC must be aded in test run
|
||||
def test_Package_Installed_Size_Not_NULL(self):
|
||||
packages = Package.objects.filter(
|
||||
installed_size__isnull=True).values('id')
|
||||
cnt_err = []
|
||||
for package in packages:
|
||||
cnt_err.append(package['id'])
|
||||
self.assertEqual(
|
||||
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
|
||||
|
||||
def test_custom_packages_generated(self):
|
||||
"""Test if there is a corresponding generated CustomImagePackage"""
|
||||
""" for each of the packages generated"""
|
||||
missing_packages = []
|
||||
|
||||
for package in Package.objects.all():
|
||||
try:
|
||||
CustomImagePackage.objects.get(name=package.name)
|
||||
except CustomImagePackage.DoesNotExist:
|
||||
missing_packages.append(package.name)
|
||||
|
||||
self.assertEqual(len(missing_packages), 0,
|
||||
"Some package were created from the build but their"
|
||||
" corresponding CustomImagePackage was not found")
|
||||
@@ -0,0 +1,49 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# 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):
|
||||
management.call_command('loaddata', 'poky')
|
||||
|
||||
num_releases = Release.objects.count()
|
||||
|
||||
self.assertTrue(
|
||||
Layer_Version.objects.filter(
|
||||
layer__name="meta-poky").count() == num_releases,
|
||||
"Loaded poky fixture but don't have a meta-poky for all releases"
|
||||
" defined")
|
||||
|
||||
def test_run_loaddata_oecore_command(self):
|
||||
management.call_command('loaddata', 'oe-core')
|
||||
|
||||
# We only have the one layer for oe-core setup
|
||||
self.assertTrue(
|
||||
Layer.objects.filter(name="openembedded-core").count() > 0,
|
||||
"Loaded oe-core fixture but still have no openemebedded-core"
|
||||
" layer")
|
||||
|
||||
def test_run_loaddata_settings_command(self):
|
||||
management.call_command('loaddata', 'settings')
|
||||
|
||||
self.assertTrue(
|
||||
ToasterSetting.objects.filter(name="DEFAULT_RELEASE").count() > 0,
|
||||
"Loaded settings but have no DEFAULT_RELEASE")
|
||||
|
||||
self.assertTrue(
|
||||
ToasterSetting.objects.filter(
|
||||
name__startswith="DEFCONF").count() > 0,
|
||||
"Loaded settings but have no DEFCONF (default project "
|
||||
"configuration values)")
|
||||
@@ -0,0 +1,34 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# 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
|
||||
management.call_command('loaddata', 'poky')
|
||||
|
||||
old_layers_count = Layer_Version.objects.count()
|
||||
old_recipes_count = Recipe.objects.count()
|
||||
old_machines_count = Machine.objects.count()
|
||||
|
||||
# Now fetch the metadata from the layer index
|
||||
management.call_command('lsupdates')
|
||||
|
||||
self.assertTrue(Layer_Version.objects.count() > old_layers_count,
|
||||
"lsupdates ran but we still have no more layers!")
|
||||
self.assertTrue(Recipe.objects.count() > old_recipes_count,
|
||||
"lsupdates ran but we still have no more Recipes!")
|
||||
self.assertTrue(Machine.objects.count() > old_machines_count,
|
||||
"lsupdates ran but we still have no more Machines!")
|
||||
@@ -0,0 +1,81 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core import management
|
||||
|
||||
from orm.models import signal_runbuilds
|
||||
|
||||
import threading
|
||||
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.daemon = True
|
||||
|
||||
def run(self):
|
||||
time.sleep(5)
|
||||
signal_runbuilds()
|
||||
time.sleep(1)
|
||||
|
||||
pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."),
|
||||
".runbuilds.pid")
|
||||
|
||||
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):
|
||||
""" Sanity test that runbuilds executes OK """
|
||||
|
||||
def setUp(self):
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
|
||||
"toastermain.settings_test")
|
||||
os.environ.setdefault("BUILDDIR",
|
||||
"/tmp/")
|
||||
|
||||
# Setup a real database if needed for runbuilds process
|
||||
# to connect to
|
||||
management.call_command('migrate')
|
||||
|
||||
def test_runbuilds_command(self):
|
||||
kill_runbuilds = KillRunbuilds()
|
||||
kill_runbuilds.start()
|
||||
|
||||
manage_py = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
"manage.py")
|
||||
|
||||
command = "%s runbuilds" % manage_py
|
||||
|
||||
process = subprocess.Popen(command,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
(out, err) = process.communicate()
|
||||
process.wait()
|
||||
|
||||
self.assertNotEqual(process.returncode, 1,
|
||||
"Runbuilds returned an error %s" % err)
|
||||
58
sources/poky/bitbake/lib/toaster/tests/db/test_db.py
Normal file
58
sources/poky/bitbake/lib/toaster/tests/db/test_db.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Damien Lespiau
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.core import management
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture(command, *args, **kwargs):
|
||||
out, sys.stdout = sys.stdout, StringIO()
|
||||
command(*args, **kwargs)
|
||||
sys.stdout.seek(0)
|
||||
yield sys.stdout.read()
|
||||
sys.stdout = out
|
||||
|
||||
|
||||
def makemigrations():
|
||||
management.call_command('makemigrations')
|
||||
|
||||
@pytest.mark.order(1)
|
||||
class MigrationTest(TestCase):
|
||||
|
||||
def testPendingMigration(self):
|
||||
"""Make sure there's no pending migration."""
|
||||
|
||||
with capture(makemigrations) as output:
|
||||
self.assertEqual(output, "No changes detected\n")
|
||||
22
sources/poky/bitbake/lib/toaster/tests/eventreplay/README
Normal file
22
sources/poky/bitbake/lib/toaster/tests/eventreplay/README
Normal file
@@ -0,0 +1,22 @@
|
||||
# Running eventreplay tests
|
||||
|
||||
These tests use event log files produced by bitbake <target> -w <event log file>
|
||||
You need to have event log files produced before running this tests.
|
||||
|
||||
At the moment of writing this document tests use 2 event log files: zlib.events
|
||||
and core-image-minimal.events. They're not provided with the tests due to their
|
||||
significant size.
|
||||
|
||||
Here is how to produce them:
|
||||
|
||||
$ . oe-init-build-env
|
||||
$ rm -r tmp sstate-cache
|
||||
$ bitbake core-image-minimal -w core-image-minimal.events
|
||||
$ rm -rf tmp sstate-cache
|
||||
$ bitbake zlib -w zlib.events
|
||||
|
||||
After that it should be possible to run eventreplay tests this way:
|
||||
|
||||
$ EVENTREPLAY_DIR=./ DJANGO_SETTINGS_MODULE=toastermain.settings_test ../bitbake/lib/toaster/manage.py test -v2 tests.eventreplay
|
||||
|
||||
Note that environment variable EVENTREPLAY_DIR should point to the directory with event log files.
|
||||
@@ -0,0 +1,85 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster Implementation
|
||||
#
|
||||
# Copyright (C) 2016 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
|
||||
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
|
||||
|
||||
"""
|
||||
Test toaster backend by playing build event log files
|
||||
using toaster-eventreplay script
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from subprocess import getstatusoutput
|
||||
from pathlib import Path
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from orm.models import Target_Installed_Package, Package, Build
|
||||
|
||||
class EventReplay(TestCase):
|
||||
"""Base class for eventreplay test cases"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup build environment:
|
||||
- set self.script to toaster-eventreplay path
|
||||
- set self.eventplay_dir to the value of EVENTPLAY_DIR env variable
|
||||
"""
|
||||
bitbake_dir = Path(__file__.split('lib/toaster')[0])
|
||||
self.script = bitbake_dir / 'bin' / 'toaster-eventreplay'
|
||||
self.assertTrue(self.script.exists(), "%s doesn't exist")
|
||||
self.eventplay_dir = os.getenv("EVENTREPLAY_DIR")
|
||||
self.assertTrue(self.eventplay_dir,
|
||||
"Environment variable EVENTREPLAY_DIR is not set")
|
||||
|
||||
def _replay(self, eventfile):
|
||||
"""Run toaster-eventplay <eventfile>"""
|
||||
eventpath = Path(self.eventplay_dir) / eventfile
|
||||
status, output = getstatusoutput('%s %s' % (self.script, eventpath))
|
||||
if status:
|
||||
print(output)
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
|
||||
class CoreImageMinimalEventReplay(EventReplay):
|
||||
"""Replay core-image-minimal events"""
|
||||
|
||||
def test_installed_packages(self):
|
||||
"""Test if all required packages have been installed"""
|
||||
|
||||
self._replay('core-image-minimal.events')
|
||||
|
||||
# test installed packages
|
||||
packages = sorted(Target_Installed_Package.objects.\
|
||||
values_list('package__name', flat=True))
|
||||
self.assertEqual(packages, ['base-files', 'base-passwd', 'busybox',
|
||||
'busybox-hwclock', 'busybox-syslog',
|
||||
'busybox-udhcpc', 'eudev', 'glibc',
|
||||
'init-ifupdown', 'initscripts',
|
||||
'initscripts-functions', 'kernel-base',
|
||||
'kernel-module-uvesafb', 'libkmod',
|
||||
'modutils-initscripts', 'netbase',
|
||||
'packagegroup-core-boot', 'run-postinsts',
|
||||
'sysvinit', 'sysvinit-inittab',
|
||||
'sysvinit-pidof', 'udev-cache',
|
||||
'update-alternatives-opkg',
|
||||
'update-rc.d', 'util-linux-libblkid',
|
||||
'util-linux-libuuid', 'v86d', 'zlib'])
|
||||
|
||||
class ZlibEventReplay(EventReplay):
|
||||
"""Replay zlib events"""
|
||||
|
||||
def test_replay_zlib(self):
|
||||
"""Test if zlib build and package are in the database"""
|
||||
self._replay("zlib.events")
|
||||
|
||||
self.assertEqual(Build.objects.last().target_set.last().target, "zlib")
|
||||
self.assertTrue('zlib' in Package.objects.values_list('name', flat=True))
|
||||
@@ -0,0 +1,138 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster functional tests implementation
|
||||
#
|
||||
# Copyright (C) 2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import signal
|
||||
import re
|
||||
|
||||
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
|
||||
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 = 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: "
|
||||
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
|
||||
|
||||
# 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'"
|
||||
start_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.environ.get("BUILDDIR"),
|
||||
shell=True)
|
||||
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/'
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(SeleniumFunctionalTestCase, cls).tearDownClass()
|
||||
|
||||
global toaster_processes
|
||||
|
||||
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(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
|
||||
return project_url.group(2)
|
||||
|
||||
|
||||
def find_element_by_link_text_in_table(self, table_id, link_text):
|
||||
"""
|
||||
Assume there're multiple suitable "find_element_by_link_text".
|
||||
In this circumstance we need to specify "table".
|
||||
"""
|
||||
try:
|
||||
table_element = self.get_table_element(table_id)
|
||||
element = table_element.find_element(By.LINK_TEXT, link_text)
|
||||
except NoSuchElementException:
|
||||
print('no element found')
|
||||
raise
|
||||
return element
|
||||
|
||||
def get_table_element(self, table_id, *coordinate):
|
||||
if len(coordinate) == 0:
|
||||
#return whole-table element
|
||||
element_xpath = "//*[@id='" + table_id + "']"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
raise
|
||||
return element
|
||||
row = coordinate[0]
|
||||
|
||||
if len(coordinate) == 1:
|
||||
#return whole-row element
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
#now we are looking for an element with specified X and Y
|
||||
column = coordinate[1]
|
||||
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
@@ -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_kirkstone(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 4.0 "Kirkstone" (option value: 1)
|
||||
- Merge Toaster settings: True
|
||||
"""
|
||||
release = '1'
|
||||
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_dunfell(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 3.1 "Dunfell" (option value: 5)
|
||||
- Merge Toaster settings: False
|
||||
"""
|
||||
release = '5'
|
||||
release_title = 'Yocto Project 3.1 "Dunfell"'
|
||||
project_name = 'projectdunfell'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
False,
|
||||
)
|
||||
|
||||
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")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user