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:
Siggi (OpenClaw Agent)
2026-03-01 20:58:18 +00:00
commit 16accb6b24
15086 changed files with 1292356 additions and 0 deletions

View 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)

View 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'),
]

View 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")

View 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)

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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),
),
]

View File

@@ -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')]),
),
]

View File

@@ -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')]),
),
]

View File

@@ -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')]),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View 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)

View File

@@ -0,0 +1,5 @@
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Create your views here.

View File

@@ -0,0 +1 @@
*.log*

View 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)

View 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

View File

@@ -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

View 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 &lt;a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/{{h_release}}"&gt;Yocto Project {{Release}} branch&lt;/a&gt;'
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 &lt;a href=\\"https://cgit.openembedded.org/openembedded-core/log/{{h_release}}\\"&gt;OpenEmbedded {{Release}}&lt;/a&gt; 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:])

View 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 &lt;a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=kirkstone\"&gt;OpenEmbedded Kirkstone&lt;/a&gt; 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 &lt;a href=\"https://cgit.openembedded.org/openembedded-core/log/\"&gt;OpenEmbedded master&lt;/a&gt; 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 &lt;a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=mickledore\"&gt;OpenEmbedded Mickledore&lt;/a&gt; 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 &lt;a href=\"https://cgit.openembedded.org/openembedded-core/log/?h=dunfell\"&gt;OpenEmbedded Dunfell&lt;/a&gt; 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>

View 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 &lt;a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=kirkstone"&gt;Yocto Project Kirkstone branch&lt;/a&gt;.</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 &lt;a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/"&gt;Yocto Project Master branch&lt;/a&gt;.</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 &lt;a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=mickledore"&gt;Yocto Project Mickledore branch&lt;/a&gt;.</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 &lt;a href="https://git.yoctoproject.org/cgit/cgit.cgi/poky/log/?h=dunfell"&gt;Yocto Project Dunfell branch&lt;/a&gt;.</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>

View 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>

View File

@@ -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()

View 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')]),
),
]

View File

@@ -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',),
),
]

View File

@@ -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',),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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')]),
),
]

View File

@@ -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')]),
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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',
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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)
),
]

View File

@@ -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)),
],
),
]

View File

@@ -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)
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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)),
],
),
]

File diff suppressed because it is too large Load Diff

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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')

View File

@@ -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)

View File

@@ -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")

View File

@@ -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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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')

View 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.

View 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

View File

@@ -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")

View File

@@ -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)")

View File

@@ -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!")

View File

@@ -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)

View 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")

View 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.

View File

@@ -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))

View File

@@ -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

View File

@@ -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