diff options
author | Patrick Williams <patrick@stwcx.xyz> | 2016-03-30 23:21:19 +0300 |
---|---|---|
committer | Patrick Williams <patrick@stwcx.xyz> | 2016-03-30 23:21:19 +0300 |
commit | b4a027550acf2c1051c34f997b8e7e845017af4b (patch) | |
tree | 9e38d3c17b42cb1e6765620a87e908973a93c821 /yocto-poky/bitbake/lib | |
parent | 2fe86d90044af218ced8f42fdded6b136f1046d2 (diff) | |
parent | f1e5d6968976c2341c6d554bfcc8895f1b33c26b (diff) | |
download | openbmc-b4a027550acf2c1051c34f997b8e7e845017af4b.tar.xz |
Merge commit 'f1e5d6968976c2341c6d554bfcc8895f1b33c26b' from yocto-2.0.1
Diffstat (limited to 'yocto-poky/bitbake/lib')
88 files changed, 4046 insertions, 979 deletions
diff --git a/yocto-poky/bitbake/lib/bb/__init__.py b/yocto-poky/bitbake/lib/bb/__init__.py index 1f7946e7b..ac62d262c 100644 --- a/yocto-poky/bitbake/lib/bb/__init__.py +++ b/yocto-poky/bitbake/lib/bb/__init__.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -__version__ = "1.27.1" +__version__ = "1.28.0" import sys if sys.version_info < (2, 7, 3): diff --git a/yocto-poky/bitbake/lib/bb/build.py b/yocto-poky/bitbake/lib/bb/build.py index 948c3951f..22428a649 100644 --- a/yocto-poky/bitbake/lib/bb/build.py +++ b/yocto-poky/bitbake/lib/bb/build.py @@ -413,6 +413,13 @@ def _exec_task(fn, task, d, quieterr): nice = int(nice) - curnice newnice = os.nice(nice) logger.debug(1, "Renice to %s " % newnice) + ionice = localdata.getVar("BB_TASK_IONICE_LEVEL", True) + if ionice: + try: + cls, prio = ionice.split(".", 1) + bb.utils.ioprio_set(os.getpid(), int(cls), int(prio)) + except: + bb.warn("Invalid ionice level %s" % ionice) bb.utils.mkdirhier(tempdir) diff --git a/yocto-poky/bitbake/lib/bb/command.py b/yocto-poky/bitbake/lib/bb/command.py index 398c1d6a6..74106d143 100644 --- a/yocto-poky/bitbake/lib/bb/command.py +++ b/yocto-poky/bitbake/lib/bb/command.py @@ -181,6 +181,16 @@ class CommandsSync: value = str(params[1]) command.cooker.data.setVar(varname, value) + def getSetVariable(self, command, params): + """ + Read the value of a variable from data and set it into the datastore + which effectively expands and locks the value. + """ + varname = params[0] + result = self.getVariable(command, params) + command.cooker.data.setVar(varname, result) + return result + def setConfig(self, command, params): """ Set the value of variable in configuration diff --git a/yocto-poky/bitbake/lib/bb/cooker.py b/yocto-poky/bitbake/lib/bb/cooker.py index a0d7d59ea..4df88818f 100644 --- a/yocto-poky/bitbake/lib/bb/cooker.py +++ b/yocto-poky/bitbake/lib/bb/cooker.py @@ -255,6 +255,11 @@ class BBCooker: self.state = state.initial self.caches_array = [] + # Need to preserve BB_CONSOLELOG over resets + consolelog = None + if hasattr(self, "data"): + consolelog = self.data.getVar("BB_CONSOLELOG", True) + if CookerFeatures.BASEDATASTORE_TRACKING in self.featureset: self.enableDataTracking() @@ -281,6 +286,8 @@ class BBCooker: self.data = self.databuilder.data self.data_hash = self.databuilder.data_hash + if consolelog: + self.data.setVar("BB_CONSOLELOG", consolelog) # we log all events to a file if so directed if self.configuration.writeeventlog: @@ -531,6 +538,11 @@ class BBCooker: for o in options: if o in ['prefile', 'postfile']: clean = False + server_val = getattr(self.configuration, "%s_server" % o) + if not options[o] and server_val: + # restore value provided on server start + setattr(self.configuration, o, server_val) + continue setattr(self.configuration, o, options[o]) for k in bb.utils.approved_variables(): if k in environment and k not in self.configuration.env: @@ -1391,10 +1403,28 @@ class BBCooker: build.reset_cache() self.buildSetVars() + # If we are told to do the None task then query the default task + if (task == None): + task = self.configuration.cmd + + if not task.startswith("do_"): + task = "do_%s" % task + taskdata, runlist, fulltargetlist = self.buildTaskData(targets, task, self.configuration.abort) buildname = self.data.getVar("BUILDNAME", False) - bb.event.fire(bb.event.BuildStarted(buildname, fulltargetlist), self.data) + + # make targets to always look as <target>:do_<task> + ntargets = [] + for target in fulltargetlist: + if ":" in target: + if ":do_" not in target: + target = "%s:do_%s" % tuple(target.split(":", 1)) + else: + target = "%s:%s" % (target, task) + ntargets.append(target) + + bb.event.fire(bb.event.BuildStarted(buildname, ntargets), self.data) rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist) if 'universe' in targets: diff --git a/yocto-poky/bitbake/lib/bb/cookerdata.py b/yocto-poky/bitbake/lib/bb/cookerdata.py index f19c28388..671c0cb0e 100644 --- a/yocto-poky/bitbake/lib/bb/cookerdata.py +++ b/yocto-poky/bitbake/lib/bb/cookerdata.py @@ -63,9 +63,9 @@ class ConfigParameters(object): raise Exception("Unable to set configuration option 'cmd' on the server: %s" % error) if not self.options.pkgs_to_build: - bbpkgs, error = server.runCommand(["getVariable", "BBPKGS"]) + bbpkgs, error = server.runCommand(["getVariable", "BBTARGETS"]) if error: - raise Exception("Unable to get the value of BBPKGS from the server: %s" % error) + raise Exception("Unable to get the value of BBTARGETS from the server: %s" % error) if bbpkgs: self.options.pkgs_to_build.extend(bbpkgs.split()) @@ -129,6 +129,8 @@ class CookerConfiguration(object): self.extra_assume_provided = [] self.prefile = [] self.postfile = [] + self.prefile_server = [] + self.postfile_server = [] self.debug = 0 self.cmd = None self.abort = True diff --git a/yocto-poky/bitbake/lib/bb/fetch2/__init__.py b/yocto-poky/bitbake/lib/bb/fetch2/__init__.py index 288a1c8fd..a9c044b6a 100644 --- a/yocto-poky/bitbake/lib/bb/fetch2/__init__.py +++ b/yocto-poky/bitbake/lib/bb/fetch2/__init__.py @@ -955,7 +955,7 @@ def try_mirror_url(fetch, origud, ud, ld, check = False): origud.method.download(origud, ld) if hasattr(origud.method,"build_mirror_data"): origud.method.build_mirror_data(origud, ld) - return ud.localpath + return origud.localpath # Otherwise the result is a local file:// and we symlink to it if not os.path.exists(origud.localpath): if os.path.islink(origud.localpath): diff --git a/yocto-poky/bitbake/lib/bb/fetch2/hg.py b/yocto-poky/bitbake/lib/bb/fetch2/hg.py index bbb4ed95d..3b743ff51 100644 --- a/yocto-poky/bitbake/lib/bb/fetch2/hg.py +++ b/yocto-poky/bitbake/lib/bb/fetch2/hg.py @@ -28,6 +28,7 @@ import os import sys import logging import bb +import errno from bb import data from bb.fetch2 import FetchMethod from bb.fetch2 import FetchError diff --git a/yocto-poky/bitbake/lib/bb/fetch2/svn.py b/yocto-poky/bitbake/lib/bb/fetch2/svn.py index 1733c2beb..8a291935c 100644 --- a/yocto-poky/bitbake/lib/bb/fetch2/svn.py +++ b/yocto-poky/bitbake/lib/bb/fetch2/svn.py @@ -54,6 +54,11 @@ class Svn(FetchMethod): ud.module = ud.parm["module"] + if not "path_spec" in ud.parm: + ud.path_spec = ud.module + else: + ud.path_spec = ud.parm["path_spec"] + # Create paths to svn checkouts relpath = self._strip_leading_slashes(ud.path) ud.pkgdir = os.path.join(data.expand('${SVNDIR}', d), ud.host, relpath) @@ -102,7 +107,7 @@ class Svn(FetchMethod): if command == "fetch": transportuser = ud.parm.get("transportuser", "") - svncmd = "%s co %s %s://%s%s/%s%s %s" % (ud.basecmd, " ".join(options), proto, transportuser, svnroot, ud.module, suffix, ud.module) + svncmd = "%s co %s %s://%s%s/%s%s %s" % (ud.basecmd, " ".join(options), proto, transportuser, svnroot, ud.module, suffix, ud.path_spec) elif command == "update": svncmd = "%s update %s" % (ud.basecmd, " ".join(options)) else: @@ -149,7 +154,7 @@ class Svn(FetchMethod): os.chdir(ud.pkgdir) # tar them up to a defined filename - runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.module), d, cleanup = [ud.localpath]) + runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.path_spec), d, cleanup = [ud.localpath]) def clean(self, ud, d): """ Clean SVN specific files and dirs """ diff --git a/yocto-poky/bitbake/lib/bb/main.py b/yocto-poky/bitbake/lib/bb/main.py index 8762f7220..c8530fc3d 100755 --- a/yocto-poky/bitbake/lib/bb/main.py +++ b/yocto-poky/bitbake/lib/bb/main.py @@ -383,6 +383,13 @@ def bitbake_main(configParams, configuration): # Collect the feature set for the UI featureset = getattr(ui_module, "featureSet", []) + if configParams.server_only: + for param in ('prefile', 'postfile'): + value = getattr(configParams, param) + if value: + setattr(configuration, "%s_server" % param, value) + param = "%s_server" % param + if not configParams.remote_server: # we start a server with a given configuration server = start_server(servermodule, configParams, configuration, featureset) diff --git a/yocto-poky/bitbake/lib/bb/runqueue.py b/yocto-poky/bitbake/lib/bb/runqueue.py index 2b71eed06..878028aa9 100644 --- a/yocto-poky/bitbake/lib/bb/runqueue.py +++ b/yocto-poky/bitbake/lib/bb/runqueue.py @@ -797,6 +797,15 @@ class RunQueueData: st = "do_%s" % st invalidate_task(fn, st, True) + # Create and print to the logs a virtual/xxxx -> PN (fn) table + virtmap = taskData.get_providermap() + virtpnmap = {} + for v in virtmap: + virtpnmap[v] = self.dataCache.pkg_fn[virtmap[v]] + bb.debug(2, "%s resolved to: %s (%s)" % (v, virtpnmap[v], virtmap[v])) + if hasattr(bb.parse.siggen, "tasks_resolved"): + bb.parse.siggen.tasks_resolved(virtmap, virtpnmap, self.dataCache) + # Iterate over the task list and call into the siggen code dealtwith = set() todeal = set(range(len(self.runq_fnid))) diff --git a/yocto-poky/bitbake/lib/bb/siggen.py b/yocto-poky/bitbake/lib/bb/siggen.py index 298527221..0352e4523 100644 --- a/yocto-poky/bitbake/lib/bb/siggen.py +++ b/yocto-poky/bitbake/lib/bb/siggen.py @@ -80,6 +80,7 @@ class SignatureGeneratorBasic(SignatureGenerator): self.taskdeps = {} self.runtaskdeps = {} self.file_checksum_values = {} + self.taints = {} self.gendeps = {} self.lookupcache = {} self.pkgnameextract = re.compile("(?P<fn>.*)\..*") @@ -199,11 +200,14 @@ class SignatureGeneratorBasic(SignatureGenerator): if 'nostamp' in taskdep and task in taskdep['nostamp']: # Nostamp tasks need an implicit taint so that they force any dependent tasks to run import uuid - data = data + str(uuid.uuid4()) + taint = str(uuid.uuid4()) + data = data + taint + self.taints[k] = "nostamp:" + taint taint = self.read_taint(fn, task, dataCache.stamp[fn]) if taint: data = data + taint + self.taints[k] = taint logger.warn("%s is tainted from a forced run" % k) h = hashlib.md5(data).hexdigest() @@ -247,6 +251,10 @@ class SignatureGeneratorBasic(SignatureGenerator): if taint: data['taint'] = taint + if runtime and k in self.taints: + if 'nostamp:' in self.taints[k]: + data['taint'] = self.taints[k] + fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(sigfile), prefix="sigtask.") try: with os.fdopen(fd, "wb") as stream: diff --git a/yocto-poky/bitbake/lib/bb/taskdata.py b/yocto-poky/bitbake/lib/bb/taskdata.py index 5fab7043c..4d12b3325 100644 --- a/yocto-poky/bitbake/lib/bb/taskdata.py +++ b/yocto-poky/bitbake/lib/bb/taskdata.py @@ -612,6 +612,18 @@ class TaskData: break # self.dump_data() + def get_providermap(self): + virts = [] + virtmap = {} + + for name in self.build_names_index: + if name.startswith("virtual/"): + virts.append(name) + for v in virts: + if self.have_build_target(v): + virtmap[v] = self.fn_index[self.get_provider(v)[0]] + return virtmap + def dump_data(self): """ Dump some debug information on the internal data structures diff --git a/yocto-poky/bitbake/lib/bb/tests/utils.py b/yocto-poky/bitbake/lib/bb/tests/utils.py index 9171509a6..a035ccf17 100644 --- a/yocto-poky/bitbake/lib/bb/tests/utils.py +++ b/yocto-poky/bitbake/lib/bb/tests/utils.py @@ -376,3 +376,206 @@ do_functionname() { (updated, newlines) = bb.utils.edit_metadata(self._origfile.splitlines(True), varlist, handle_var) self.assertTrue(updated, 'List should be updated but isn\'t') self.assertEqual(newlines, newfile5.splitlines(True)) + + +class EditBbLayersConf(unittest.TestCase): + + def _test_bblayers_edit(self, before, after, add, remove, notadded, notremoved): + with tempfile.NamedTemporaryFile('w', delete=False) as tf: + tf.write(before) + tf.close() + try: + actual_notadded, actual_notremoved = bb.utils.edit_bblayers_conf(tf.name, add, remove) + with open(tf.name) as f: + actual_after = f.readlines() + self.assertEqual(after.splitlines(True), actual_after) + self.assertEqual(notadded, actual_notadded) + self.assertEqual(notremoved, actual_notremoved) + finally: + os.remove(tf.name) + + + def test_bblayers_remove(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/subpath/layer3 \ + /home/user/path/layer4 \ + " +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/subpath/layer3 \ + /home/user/path/layer4 \ + " +""" + self._test_bblayers_edit(before, after, + None, + '/home/user/path/layer2', + [], + []) + + + def test_bblayers_add(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/subpath/layer3 \ + /home/user/path/layer4 \ + " +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/subpath/layer3 \ + /home/user/path/layer4 \ + /other/path/to/layer5 \ + " +""" + self._test_bblayers_edit(before, after, + '/other/path/to/layer5/', + None, + [], + []) + + + def test_bblayers_add_remove(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/subpath/layer3 \ + /home/user/path/layer4 \ + " +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/layer4 \ + /other/path/to/layer5 \ + " +""" + self._test_bblayers_edit(before, after, + ['/other/path/to/layer5', '/home/user/path/layer2/'], '/home/user/path/subpath/layer3/', + ['/home/user/path/layer2'], + []) + + + def test_bblayers_add_remove_home(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + ~/path/layer1 \ + ~/path/layer2 \ + ~/otherpath/layer3 \ + ~/path/layer4 \ + " +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS = " \ + ~/path/layer2 \ + ~/path/layer4 \ + ~/path2/layer5 \ + " +""" + self._test_bblayers_edit(before, after, + [os.environ['HOME'] + '/path/layer4', '~/path2/layer5'], + [os.environ['HOME'] + '/otherpath/layer3', '~/path/layer1', '~/path/notinlist'], + [os.environ['HOME'] + '/path/layer4'], + ['~/path/notinlist']) + + + def test_bblayers_add_remove_plusequals(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS += " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + " +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS += " \ + /home/user/path/layer2 \ + /home/user/path/layer3 \ + " +""" + self._test_bblayers_edit(before, after, + '/home/user/path/layer3', + '/home/user/path/layer1', + [], + []) + + + def test_bblayers_add_remove_plusequals2(self): + before = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS += " \ + /home/user/path/layer1 \ + /home/user/path/layer2 \ + /home/user/path/layer3 \ + " +BBLAYERS += "/home/user/path/layer4" +BBLAYERS += "/home/user/path/layer5" +""" + after = r""" +# A comment + +BBPATH = "${TOPDIR}" +BBFILES ?= "" +BBLAYERS += " \ + /home/user/path/layer2 \ + /home/user/path/layer3 \ + " +BBLAYERS += "/home/user/path/layer5" +BBLAYERS += "/home/user/otherpath/layer6" +""" + self._test_bblayers_edit(before, after, + ['/home/user/otherpath/layer6', '/home/user/path/layer3'], ['/home/user/path/layer1', '/home/user/path/layer4', '/home/user/path/layer7'], + ['/home/user/path/layer3'], + ['/home/user/path/layer7']) diff --git a/yocto-poky/bitbake/lib/bb/tinfoil.py b/yocto-poky/bitbake/lib/bb/tinfoil.py index 1ea46d8ee..7aa653f1a 100644 --- a/yocto-poky/bitbake/lib/bb/tinfoil.py +++ b/yocto-poky/bitbake/lib/bb/tinfoil.py @@ -36,13 +36,13 @@ class Tinfoil: # Set up logging self.logger = logging.getLogger('BitBake') - console = logging.StreamHandler(output) - bb.msg.addDefaultlogFilter(console) + self._log_hdlr = logging.StreamHandler(output) + bb.msg.addDefaultlogFilter(self._log_hdlr) format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s") if output.isatty(): format.enable_color() - console.setFormatter(format) - self.logger.addHandler(console) + self._log_hdlr.setFormatter(format) + self.logger.addHandler(self._log_hdlr) self.config = CookerConfiguration() configparams = TinfoilConfigParameters(parse_only=True) @@ -88,6 +88,7 @@ class Tinfoil: self.cooker.shutdown(force=True) self.cooker.post_serve() self.cooker.unlockBitbake() + self.logger.removeHandler(self._log_hdlr) class TinfoilConfigParameters(ConfigParameters): diff --git a/yocto-poky/bitbake/lib/bb/ui/buildinfohelper.py b/yocto-poky/bitbake/lib/bb/ui/buildinfohelper.py index 6e313fee8..78f1e9274 100644 --- a/yocto-poky/bitbake/lib/bb/ui/buildinfohelper.py +++ b/yocto-poky/bitbake/lib/bb/ui/buildinfohelper.py @@ -66,6 +66,7 @@ class ORMWrapper(object): def __init__(self): self.layer_version_objects = [] + self.layer_version_built = [] self.task_objects = {} self.recipe_objects = {} @@ -94,8 +95,8 @@ class ORMWrapper(object): created = False if not key in vars(self)[dictname].keys(): - vars(self)[dictname][key] = clazz.objects.create(**kwargs) - created = True + vars(self)[dictname][key], created = \ + clazz.objects.get_or_create(**kwargs) return (vars(self)[dictname][key], created) @@ -161,8 +162,6 @@ class ORMWrapper(object): build.bitbake_version=build_info['bitbake_version'] build.save() - Target.objects.filter(build = build).delete() - else: build = Build.objects.create( project = prj, @@ -183,18 +182,26 @@ class ORMWrapper(object): return build - def create_target_objects(self, target_info): - assert 'build' in target_info - assert 'targets' in target_info - - targets = [] - for tgt_name in target_info['targets']: - tgt_object = Target.objects.create( build = target_info['build'], - target = tgt_name, - is_image = False, - ) - targets.append(tgt_object) - return targets + @staticmethod + def get_or_create_targets(target_info): + result = [] + for target in target_info['targets']: + task = '' + if ':' in target: + target, task = target.split(':', 1) + if task.startswith('do_'): + task = task[3:] + if task == 'build': + task = '' + obj, created = Target.objects.get_or_create(build=target_info['build'], + target=target) + if created: + obj.is_image = False + if task: + obj.task = task + obj.save() + result.append(obj) + return result def update_build_object(self, build, errors, warnings, taskfailures): assert isinstance(build,Build) @@ -269,23 +276,66 @@ class ORMWrapper(object): assert not recipe_information['file_path'].startswith("/") # we should have layer-relative paths at all times - recipe_object, created = self._cached_get_or_create(Recipe, layer_version=recipe_information['layer_version'], + + def update_recipe_obj(recipe_object): + object_changed = False + for v in vars(recipe_object): + if v in recipe_information.keys(): + object_changed = True + vars(recipe_object)[v] = recipe_information[v] + + if object_changed: + recipe_object.save() + + recipe, created = self._cached_get_or_create(Recipe, layer_version=recipe_information['layer_version'], file_path=recipe_information['file_path'], pathflags = recipe_information['pathflags']) - if created and must_exist: - raise NotExisting("Recipe object created when expected to exist", recipe_information) - object_changed = False - for v in vars(recipe_object): - if v in recipe_information.keys(): - object_changed = True - vars(recipe_object)[v] = recipe_information[v] + update_recipe_obj(recipe) - if object_changed: - recipe_object.save() + built_recipe = None + # Create a copy of the recipe for historical puposes and update it + for built_layer in self.layer_version_built: + if built_layer.layer == recipe_information['layer_version'].layer: + built_recipe, c = self._cached_get_or_create(Recipe, + layer_version=built_layer, + file_path=recipe_information['file_path'], + pathflags = recipe_information['pathflags']) + update_recipe_obj(built_recipe) + break - return recipe_object + + # If we're in analysis mode then we are wholly responsible for the data + # and therefore we return the 'real' recipe rather than the build + # history copy of the recipe. + if recipe_information['layer_version'].build is not None and \ + recipe_information['layer_version'].build.project == \ + Project.objects.get_default_project(): + return recipe + + return built_recipe def get_update_layer_version_object(self, build_obj, layer_obj, layer_version_information): + if isinstance(layer_obj, Layer_Version): + # We already found our layer version for this build so just + # update it with the new build information + logger.debug("We found our layer from toaster") + layer_obj.local_path = layer_version_information['local_path'] + layer_obj.save() + self.layer_version_objects.append(layer_obj) + + # create a new copy of this layer version as a snapshot for + # historical purposes + layer_copy, c = Layer_Version.objects.get_or_create(build=build_obj, + layer=layer_obj.layer, + commit=layer_version_information['commit'], + local_path = layer_version_information['local_path'], + ) + logger.info("created new historical layer version %d", layer_copy.pk) + + self.layer_version_built.append(layer_copy) + + return layer_obj + assert isinstance(build_obj, Build) assert isinstance(layer_obj, Layer) assert 'branch' in layer_version_information @@ -293,14 +343,20 @@ class ORMWrapper(object): assert 'priority' in layer_version_information assert 'local_path' in layer_version_information + # If we're doing a command line build then associate this new layer with the + # project to avoid it 'contaminating' toaster data + project = None + if build_obj.project == Project.objects.get_default_project(): + project = build_obj.project + layer_version_object, _ = Layer_Version.objects.get_or_create( - build = build_obj, - layer = layer_obj, - branch = layer_version_information['branch'], - commit = layer_version_information['commit'], - priority = layer_version_information['priority'], - local_path = layer_version_information['local_path'], - ) + build = build_obj, + layer = layer_obj, + branch = layer_version_information['branch'], + commit = layer_version_information['commit'], + priority = layer_version_information['priority'], + local_path = layer_version_information['local_path'], + project=project) self.layer_version_objects.append(layer_version_object) @@ -335,8 +391,15 @@ class ORMWrapper(object): localdirname = os.path.join(bc.be.sourcedir, localdirname) #logger.debug(1, "Localdirname %s lcal_path %s" % (localdirname, layer_information['local_path'])) if localdirname.startswith(layer_information['local_path']): + # If the build request came from toaster this field + # should contain the information from the layer_version + # That created this build request. + if brl.layer_version: + return brl.layer_version + # we matched the BRLayer, but we need the layer_version that generated this BR; reverse of the Project.schedule_build() #logger.debug(1, "Matched %s to BRlayer %s" % (pformat(layer_information["local_path"]), localdirname)) + for pl in buildrequest.project.projectlayer_set.filter(layercommit__layer__name = brl.name): if pl.layercommit.layer.vcs_url == brl.giturl : layer = pl.layercommit.layer @@ -353,26 +416,29 @@ class ORMWrapper(object): files = filedata['files'] syms = filedata['syms'] - # we insert directories, ordered by name depth + # always create the root directory as a special case; + # note that this is never displayed, so the owner, group, + # size, permission are irrelevant + tf_obj = Target_File.objects.create(target = target_obj, + path = '/', + size = 0, + owner = '', + group = '', + permission = '', + inodetype = Target_File.ITYPE_DIRECTORY) + tf_obj.save() + + # insert directories, ordered by name depth for d in sorted(dirs, key=lambda x:len(x[-1].split("/"))): (user, group, size) = d[1:4] permission = d[0][1:] path = d[4].lstrip(".") + + # we already created the root directory, so ignore any + # entry for it if len(path) == 0: - # we create the root directory as a special case - path = "/" - tf_obj = Target_File.objects.create( - target = target_obj, - path = path, - size = size, - inodetype = Target_File.ITYPE_DIRECTORY, - permission = permission, - owner = user, - group = group, - ) - tf_obj.directory = tf_obj - tf_obj.save() continue + parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1]) if len(parent_path) == 0: parent_path = "/" @@ -461,6 +527,12 @@ class ORMWrapper(object): errormsg = "" for p in packagedict: searchname = p + if p not in pkgpnmap: + logger.warning("Image packages list contains %p, but is" + " missing from all packages list where the" + " metadata comes from. Skipping...", p) + continue + if 'OPKGN' in pkgpnmap[p].keys(): searchname = pkgpnmap[p]['OPKGN'] @@ -504,13 +576,20 @@ class ORMWrapper(object): elif deptype == 'recommends': tdeptype = Package_Dependency.TYPE_TRECOMMENDS - packagedeps_objs.append(Package_Dependency( package = packagedict[p]['object'], - depends_on = packagedict[px]['object'], - dep_type = tdeptype, - target = target_obj)) + try: + packagedeps_objs.append(Package_Dependency( + package = packagedict[p]['object'], + depends_on = packagedict[px]['object'], + dep_type = tdeptype, + target = target_obj)) + except KeyError as e: + logger.warn("Could not add dependency to the package %s " + "because %s is an unknown package", p, px) if len(packagedeps_objs) > 0: Package_Dependency.objects.bulk_create(packagedeps_objs) + else: + logger.info("No package dependencies created") if len(errormsg) > 0: logger.warn("buildinfohelper: target_package_info could not identify recipes: \n%s", errormsg) @@ -686,6 +765,7 @@ class BuildInfoHelper(object): def __init__(self, server, has_build_history = False): self.internal_state = {} self.internal_state['taskdata'] = {} + self.internal_state['targets'] = [] self.task_order = 0 self.autocommit_step = 1 self.server = server @@ -704,7 +784,7 @@ class BuildInfoHelper(object): ## methods to convert event/external info into objects that the ORM layer uses - def _get_build_information(self, consolelogfile): + def _get_build_information(self, build_log_path): build_info = {} # Generate an identifier for each new build @@ -713,7 +793,7 @@ class BuildInfoHelper(object): build_info['distro_version'] = self.server.runCommand(["getVariable", "DISTRO_VERSION"])[0] build_info['started_on'] = timezone.now() build_info['completed_on'] = timezone.now() - build_info['cooker_log_path'] = consolelogfile + build_info['cooker_log_path'] = build_log_path build_info['build_name'] = self.server.runCommand(["getVariable", "BUILDNAME"])[0] build_info['bitbake_version'] = self.server.runCommand(["getVariable", "BB_VERSION"])[0] @@ -764,8 +844,15 @@ class BuildInfoHelper(object): if not localdirname.startswith("/"): localdirname = os.path.join(bc.be.sourcedir, localdirname) if path.startswith(localdirname): + # If the build request came from toaster this field + # should contain the information from the layer_version + # That created this build request. + if brl.layer_version: + return brl.layer_version + #logger.warn("-- managed: matched path %s with layer %s " % (path, localdirname)) # we matched the BRLayer, but we need the layer_version that generated this br + for lvo in self.orm_wrapper.layer_version_objects: if brl.name == lvo.layer.name: return lvo @@ -774,7 +861,7 @@ class BuildInfoHelper(object): logger.warn("Could not match layer version for recipe path %s : %s", path, self.orm_wrapper.layer_version_objects) #mockup the new layer - unknown_layer, _ = Layer.objects.get_or_create(name="__FIXME__unidentified_layer", layer_index_url="") + unknown_layer, _ = Layer.objects.get_or_create(name="Unidentified layer", layer_index_url="") unknown_layer_version_obj, _ = Layer_Version.objects.get_or_create(layer = unknown_layer, build = self.internal_state['build']) # append it so we don't run into this error again and again @@ -847,9 +934,9 @@ class BuildInfoHelper(object): logger.warn("buildinfohelper: cannot identify layer exception:%s ", nee) - def store_started_build(self, event, consolelogfile): + def store_started_build(self, event, build_log_path): assert '_pkgs' in vars(event) - build_information = self._get_build_information(consolelogfile) + build_information = self._get_build_information(build_log_path) build_obj = self.orm_wrapper.create_build_object(build_information, self.brbe, self.project) @@ -869,7 +956,7 @@ class BuildInfoHelper(object): target_information['targets'] = event._pkgs target_information['build'] = build_obj - self.internal_state['targets'] = self.orm_wrapper.create_target_objects(target_information) + self.internal_state['targets'] = self.orm_wrapper.get_or_create_targets(target_information) # Save build configuration data = self.server.runCommand(["getAllKeysWithFlags", ["doc", "func"]])[0] @@ -996,7 +1083,7 @@ class BuildInfoHelper(object): task_information['disk_io'] = taskstats['disk_io'] if 'elapsed_time' in taskstats: task_information['elapsed_time'] = taskstats['elapsed_time'] - self.orm_wrapper.get_update_task_object(task_information, True) # must exist + self.orm_wrapper.get_update_task_object(task_information) def update_and_store_task(self, event): assert 'taskfile' in vars(event) @@ -1097,15 +1184,22 @@ class BuildInfoHelper(object): # for all image targets for target in self.internal_state['targets']: if target.is_image: + pkgdata = BuildInfoHelper._get_data_from_event(event)['pkgdata'] + imgdata = BuildInfoHelper._get_data_from_event(event)['imgdata'][target.target] + filedata = BuildInfoHelper._get_data_from_event(event)['filedata'][target.target] + try: - pkgdata = BuildInfoHelper._get_data_from_event(event)['pkgdata'] - imgdata = BuildInfoHelper._get_data_from_event(event)['imgdata'][target.target] self.orm_wrapper.save_target_package_information(self.internal_state['build'], target, imgdata, pkgdata, self.internal_state['recipes']) - filedata = BuildInfoHelper._get_data_from_event(event)['filedata'][target.target] + except KeyError as e: + logger.warn("KeyError in save_target_package_information" + "%s ", e) + + try: self.orm_wrapper.save_target_file_information(self.internal_state['build'], target, filedata) - except KeyError: - # we must have not got the data for this image, nothing to save - pass + except KeyError as e: + logger.warn("KeyError in save_target_file_information" + "%s ", e) + @@ -1306,7 +1400,9 @@ class BuildInfoHelper(object): log_information = {} log_information['build'] = self.internal_state['build'] - if event.levelno == formatter.ERROR: + if event.levelno == formatter.CRITICAL: + log_information['level'] = LogMessage.CRITICAL + elif event.levelno == formatter.ERROR: log_information['level'] = LogMessage.ERROR elif event.levelno == formatter.WARNING: log_information['level'] = LogMessage.WARNING @@ -1319,6 +1415,7 @@ class BuildInfoHelper(object): log_information['pathname'] = event.pathname log_information['lineno'] = event.lineno logger.info("Logging error 2: %s", log_information) + self.orm_wrapper.create_logmessage(log_information) def close(self, errorcode): diff --git a/yocto-poky/bitbake/lib/bb/ui/knotty.py b/yocto-poky/bitbake/lib/bb/ui/knotty.py index 2bee242eb..90c318376 100644 --- a/yocto-poky/bitbake/lib/bb/ui/knotty.py +++ b/yocto-poky/bitbake/lib/bb/ui/knotty.py @@ -230,7 +230,7 @@ def _log_settings_from_server(server): if error: logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) raise BaseException(error) - consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"]) + consolelogfile, error = server.runCommand(["getSetVariable", "BB_CONSOLELOG"]) if error: logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error) raise BaseException(error) diff --git a/yocto-poky/bitbake/lib/bb/ui/toasterui.py b/yocto-poky/bitbake/lib/bb/ui/toasterui.py index e0c278bb3..3d261503e 100644 --- a/yocto-poky/bitbake/lib/bb/ui/toasterui.py +++ b/yocto-poky/bitbake/lib/bb/ui/toasterui.py @@ -21,6 +21,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import division +import time import sys try: import bb @@ -43,8 +44,6 @@ featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES, bb.cooker.CookerFeature logger = logging.getLogger("ToasterLogger") interactive = sys.stdout.isatty() - - def _log_settings_from_server(server): # Get values of variables which control our output includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"]) @@ -59,12 +58,56 @@ def _log_settings_from_server(server): if error: logger.error("Unable to get the value of BB_CONSOLELOG variable: %s", error) raise BaseException(error) - return includelogs, loglines, consolelogfile + return consolelogfile + +# create a log file for a single build and direct the logger at it; +# log file name is timestamped to the millisecond (depending +# on system clock accuracy) to ensure it doesn't overlap with +# other log file names +# +# returns (log file, path to log file) for a build +def _open_build_log(log_dir): + format_str = "%(levelname)s: %(message)s" + + now = time.time() + now_ms = int((now - int(now)) * 1000) + time_str = time.strftime('build_%Y%m%d_%H%M%S', time.localtime(now)) + log_file_name = time_str + ('.%d.log' % now_ms) + build_log_file_path = os.path.join(log_dir, log_file_name) + + build_log = logging.FileHandler(build_log_file_path) + + logformat = bb.msg.BBLogFormatter(format_str) + build_log.setFormatter(logformat) + bb.msg.addDefaultlogFilter(build_log) + logger.addHandler(build_log) + + return (build_log, build_log_file_path) + +# stop logging to the build log if it exists +def _close_build_log(build_log): + if build_log: + build_log.flush() + build_log.close() + logger.removeHandler(build_log) + +def main(server, eventHandler, params): + # set to a logging.FileHandler instance when a build starts; + # see _open_build_log() + build_log = None + + # set to the log path when a build starts + build_log_file_path = None -def main(server, eventHandler, params ): helper = uihelper.BBUIHelper() + # TODO don't use log output to determine when bitbake has started + # + # WARNING: this log handler cannot be removed, as localhostbecontroller + # relies on output in the toaster_ui.log file to determine whether + # the bitbake server has started, which only happens if + # this logger is setup here (see the TODO in the loop below) console = logging.StreamHandler(sys.stdout) format_str = "%(levelname)s: %(message)s" formatter = bb.msg.BBLogFormatter(format_str) @@ -73,8 +116,6 @@ def main(server, eventHandler, params ): logger.addHandler(console) logger.setLevel(logging.INFO) - _, _, consolelogfile = _log_settings_from_server(server) - # verify and warn build_history_enabled = True inheritlist, _ = server.runCommand(["getVariable", "INHERIT"]) @@ -87,8 +128,9 @@ def main(server, eventHandler, params ): logger.error("ToasterUI can only work in observer mode") return 1 - + # set to 1 when toasterui needs to shut down main.shutdown = 0 + interrupted = False return_value = 0 errors = 0 @@ -98,25 +140,31 @@ def main(server, eventHandler, params ): buildinfohelper = BuildInfoHelper(server, build_history_enabled) - if buildinfohelper.brbe is not None and consolelogfile: - # if we are under managed mode we have no other UI and we need to write our own file - bb.utils.mkdirhier(os.path.dirname(consolelogfile)) - conlogformat = bb.msg.BBLogFormatter(format_str) - consolelog = logging.FileHandler(consolelogfile) - bb.msg.addDefaultlogFilter(consolelog) - consolelog.setFormatter(conlogformat) - logger.addHandler(consolelog) - + # write our own log files into bitbake's log directory; + # we're only interested in the path to the parent directory of + # this file, as we're writing our own logs into the same directory + consolelogfile = _log_settings_from_server(server) + log_dir = os.path.dirname(consolelogfile) + bb.utils.mkdirhier(log_dir) while True: try: event = eventHandler.waitEvent(0.25) if first: first = False + + # TODO don't use log output to determine when bitbake has started + # + # this is the line localhostbecontroller needs to + # see in toaster_ui.log which it uses to decide whether + # the bitbake server has started... logger.info("ToasterUI waiting for events") if event is None: if main.shutdown > 0: + # if shutting down, close any open build log first + _close_build_log(build_log) + break continue @@ -125,19 +173,32 @@ def main(server, eventHandler, params ): # pylint: disable=protected-access # the code will look into the protected variables of the event; no easy way around this + # we treat ParseStarted as the first event of toaster-triggered + # builds; that way we get the Build Configuration included in the log + # and any errors that occur before BuildStarted is fired + if isinstance(event, bb.event.ParseStarted): + if not (build_log and build_log_file_path): + build_log, build_log_file_path = _open_build_log(log_dir) + continue + if isinstance(event, bb.event.BuildStarted): - buildinfohelper.store_started_build(event, consolelogfile) + # command-line builds don't fire a ParseStarted event, + # so we have to start the log file for those on BuildStarted instead + if not (build_log and build_log_file_path): + build_log, build_log_file_path = _open_build_log(log_dir) + + buildinfohelper.store_started_build(event, build_log_file_path) if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)): buildinfohelper.update_and_store_task(event) - logger.warn("Logfile for task %s", event.logfile) + logger.info("Logfile for task %s", event.logfile) continue if isinstance(event, bb.build.TaskBase): logger.info(event._message) if isinstance(event, bb.event.LogExecTTY): - logger.warn(event.msg) + logger.info(event.msg) continue if isinstance(event, logging.LogRecord): @@ -145,10 +206,12 @@ def main(server, eventHandler, params ): event.levelno = formatter.ERROR buildinfohelper.store_log_event(event) + if event.levelno >= formatter.ERROR: errors = errors + 1 elif event.levelno == formatter.WARNING: warnings = warnings + 1 + # For "normal" logging conditions, don't show note logs from tasks # but do show them if the user has changed the default log level to # include verbose/debug messages @@ -169,8 +232,6 @@ def main(server, eventHandler, params ): # timing and error informations from the parsing phase in Toaster if isinstance(event, (bb.event.SanityCheckPassed, bb.event.SanityCheck)): continue - if isinstance(event, bb.event.ParseStarted): - continue if isinstance(event, bb.event.ParseProgress): continue if isinstance(event, bb.event.ParseCompleted): @@ -246,6 +307,12 @@ def main(server, eventHandler, params ): errorcode = 1 logger.error("Command execution failed: %s", event.error) + # turn off logging to the current build log + _close_build_log(build_log) + + # reset ready for next BuildStarted + build_log = None + # update the build info helper on BuildCompleted, not on CommandXXX buildinfohelper.update_build_information(event, errors, warnings, taskfailures) buildinfohelper.close(errorcode) @@ -254,7 +321,6 @@ def main(server, eventHandler, params ): # we start a new build info if buildinfohelper.brbe is not None: - logger.debug("ToasterUI under BuildEnvironment management - exiting after the build") server.terminateServer() else: @@ -296,8 +362,9 @@ def main(server, eventHandler, params ): continue if isinstance(event, bb.cooker.CookerExit): - # exit when the server exits - break + # shutdown when bitbake server shuts down + main.shutdown = 1 + continue # ignore if isinstance(event, (bb.event.BuildBase, @@ -308,14 +375,15 @@ def main(server, eventHandler, params ): bb.event.OperationProgress, bb.command.CommandFailed, bb.command.CommandExit, - bb.command.CommandCompleted)): + bb.command.CommandCompleted, + bb.event.ReachableStamps)): continue if isinstance(event, bb.event.DepTreeGenerated): buildinfohelper.store_dependency_information(event) continue - logger.error("Unknown event: %s", event) + logger.warn("Unknown event: %s", event) return_value += 1 except EnvironmentError as ioerror: @@ -335,7 +403,7 @@ def main(server, eventHandler, params ): if tb is not None: curr = tb while curr is not None: - logger.warn("Error data dump %s\n%s\n" , traceback.format_tb(curr,1), pformat(curr.tb_frame.f_locals)) + logger.error("Error data dump %s\n%s\n" , traceback.format_tb(curr,1), pformat(curr.tb_frame.f_locals)) curr = curr.tb_next # save them to database, if possible; if it fails, we already logged to console. @@ -347,9 +415,8 @@ def main(server, eventHandler, params ): # make sure we return with an error return_value += 1 - if interrupted: - if return_value == 0: - return_value += 1 + if interrupted and return_value == 0: + return_value += 1 logger.warn("Return value is %d", return_value) return return_value diff --git a/yocto-poky/bitbake/lib/bb/utils.py b/yocto-poky/bitbake/lib/bb/utils.py index 91faa494c..31ec2b7c9 100644 --- a/yocto-poky/bitbake/lib/bb/utils.py +++ b/yocto-poky/bitbake/lib/bb/utils.py @@ -1177,7 +1177,7 @@ def edit_metadata(meta_lines, variables, varfunc, match_overrides=False): if not skip: if checkspc: checkspc = False - if newlines[-1] == '\n' and line == '\n': + if newlines and newlines[-1] == '\n' and line == '\n': # Squash blank line if there are two consecutive blanks after a removal continue newlines.append(line) @@ -1201,7 +1201,19 @@ def edit_metadata_file(meta_file, variables, varfunc): def edit_bblayers_conf(bblayers_conf, add, remove): - """Edit bblayers.conf, adding and/or removing layers""" + """Edit bblayers.conf, adding and/or removing layers + Parameters: + bblayers_conf: path to bblayers.conf file to edit + add: layer path (or list of layer paths) to add; None or empty + list to add nothing + remove: layer path (or list of layer paths) to remove; None or + empty list to remove nothing + Returns a tuple: + notadded: list of layers specified to be added but weren't + (because they were already in the list) + notremoved: list of layers that were specified to be removed + but weren't (because they weren't in the list) + """ import fnmatch @@ -1210,6 +1222,13 @@ def edit_bblayers_conf(bblayers_conf, add, remove): pth = pth[:-1] return pth + approved = bb.utils.approved_variables() + def canonicalise_path(pth): + pth = remove_trailing_sep(pth) + if 'HOME' in approved and '~' in pth: + pth = os.path.expanduser(pth) + return pth + def layerlist_param(value): if not value: return [] @@ -1218,48 +1237,80 @@ def edit_bblayers_conf(bblayers_conf, add, remove): else: return [remove_trailing_sep(value)] - notadded = [] - notremoved = [] - addlayers = layerlist_param(add) removelayers = layerlist_param(remove) # Need to use a list here because we can't set non-local variables from a callback in python 2.x bblayercalls = [] + removed = [] + plusequals = False + orig_bblayers = [] + + def handle_bblayers_firstpass(varname, origvalue, op, newlines): + bblayercalls.append(op) + if op == '=': + del orig_bblayers[:] + orig_bblayers.extend([canonicalise_path(x) for x in origvalue.split()]) + return (origvalue, None, 2, False) def handle_bblayers(varname, origvalue, op, newlines): - bblayercalls.append(varname) updated = False bblayers = [remove_trailing_sep(x) for x in origvalue.split()] if removelayers: for removelayer in removelayers: - matched = False for layer in bblayers: - if fnmatch.fnmatch(layer, removelayer): + if fnmatch.fnmatch(canonicalise_path(layer), canonicalise_path(removelayer)): updated = True - matched = True bblayers.remove(layer) + removed.append(removelayer) break - if not matched: - notremoved.append(removelayer) - if addlayers: + if addlayers and not plusequals: for addlayer in addlayers: if addlayer not in bblayers: updated = True bblayers.append(addlayer) - else: - notadded.append(addlayer) + del addlayers[:] if updated: + if op == '+=' and not bblayers: + bblayers = None return (bblayers, None, 2, False) else: return (origvalue, None, 2, False) - edit_metadata_file(bblayers_conf, ['BBLAYERS'], handle_bblayers) + with open(bblayers_conf, 'r') as f: + (_, newlines) = edit_metadata(f, ['BBLAYERS'], handle_bblayers_firstpass) if not bblayercalls: raise Exception('Unable to find BBLAYERS in %s' % bblayers_conf) + # Try to do the "smart" thing depending on how the user has laid out + # their bblayers.conf file + if bblayercalls.count('+=') > 1: + plusequals = True + + removelayers_canon = [canonicalise_path(layer) for layer in removelayers] + notadded = [] + for layer in addlayers: + layer_canon = canonicalise_path(layer) + if layer_canon in orig_bblayers and not layer_canon in removelayers_canon: + notadded.append(layer) + notadded_canon = [canonicalise_path(layer) for layer in notadded] + addlayers[:] = [layer for layer in addlayers if canonicalise_path(layer) not in notadded_canon] + + (updated, newlines) = edit_metadata(newlines, ['BBLAYERS'], handle_bblayers) + if addlayers: + # Still need to add these + for addlayer in addlayers: + newlines.append('BBLAYERS += "%s"\n' % addlayer) + updated = True + + if updated: + with open(bblayers_conf, 'w') as f: + f.writelines(newlines) + + notremoved = list(set(removelayers) - set(removed)) + return (notadded, notremoved) @@ -1310,3 +1361,27 @@ def signal_on_parent_exit(signame): result = cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signum) if result != 0: raise PrCtlError('prctl failed with error code %s' % result) + +# +# Manually call the ioprio syscall. We could depend on other libs like psutil +# however this gets us enough of what we need to bitbake for now without the +# dependency +# +_unamearch = os.uname()[4] +IOPRIO_WHO_PROCESS = 1 +IOPRIO_CLASS_SHIFT = 13 + +def ioprio_set(who, cls, value): + NR_ioprio_set = None + if _unamearch == "x86_64": + NR_ioprio_set = 251 + elif _unamearch[0] == "i" and _unamearch[2:3] == "86": + NR_ioprio_set = 289 + + if NR_ioprio_set: + ioprio = value | (cls << IOPRIO_CLASS_SHIFT) + rc = cdll['libc.so.6'].syscall(NR_ioprio_set, IOPRIO_WHO_PROCESS, who, ioprio) + if rc != 0: + raise ValueError("Unable to set ioprio, syscall returned %s" % rc) + else: + bb.warn("Unable to set IO Prio for arch %s" % _unamearch) diff --git a/yocto-poky/bitbake/lib/prserv/db.py b/yocto-poky/bitbake/lib/prserv/db.py index 437958013..36c9f7b63 100644 --- a/yocto-poky/bitbake/lib/prserv/db.py +++ b/yocto-poky/bitbake/lib/prserv/db.py @@ -248,7 +248,7 @@ class PRData(object): self.connection.execute("PRAGMA journal_mode = WAL;") self._tables={} - def __del__(self): + def disconnect(self): self.connection.close() def __getitem__(self,tblname): diff --git a/yocto-poky/bitbake/lib/prserv/serv.py b/yocto-poky/bitbake/lib/prserv/serv.py index 5c0ffb992..eafc3aab7 100644 --- a/yocto-poky/bitbake/lib/prserv/serv.py +++ b/yocto-poky/bitbake/lib/prserv/serv.py @@ -3,6 +3,7 @@ import signal, time from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import threading import Queue +import socket try: import sqlite3 @@ -37,7 +38,6 @@ singleton = None class PRServer(SimpleXMLRPCServer): def __init__(self, dbfile, logfile, interface, daemon=True): ''' constructor ''' - import socket try: SimpleXMLRPCServer.__init__(self, interface, logRequests=False, allow_none=True) @@ -148,7 +148,7 @@ class PRServer(SimpleXMLRPCServer): while not self.quit: self.handle_request() self.handlerthread.join() - self.table.sync() + self.db.disconnect() logger.info("PRServer: stopping...") self.server_close() return @@ -289,7 +289,8 @@ class PRServerConnection(object): return self.host, self.port def start_daemon(dbfile, host, port, logfile): - pidfile = PIDPREFIX % (host, port) + ip = socket.gethostbyname(host) + pidfile = PIDPREFIX % (ip, port) try: pf = file(pidfile,'r') pid = int(pf.readline().strip()) @@ -302,12 +303,21 @@ def start_daemon(dbfile, host, port, logfile): % pidfile) return 1 - server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (host,port)) + server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port)) server.start() + + # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with + # the one the server actually is listening, so at least warn the user about it + _,rport = server.getinfo() + if port != rport: + sys.stdout.write("Server is listening at port %s instead of %s\n" + % (rport,port)) return 0 def stop_daemon(host, port): - pidfile = PIDPREFIX % (host, port) + import glob + ip = socket.gethostbyname(host) + pidfile = PIDPREFIX % (ip, port) try: pf = file(pidfile,'r') pid = int(pf.readline().strip()) @@ -316,11 +326,23 @@ def stop_daemon(host, port): pid = None if not pid: - sys.stderr.write("pidfile %s does not exist. Daemon not running?\n" - % pidfile) + # when server starts at port=0 (i.e. localhost:0), server actually takes another port, + # so at least advise the user which ports the corresponding server is listening + ports = [] + portstr = "" + for pf in glob.glob(PIDPREFIX % (ip,'*')): + bn = os.path.basename(pf) + root, _ = os.path.splitext(bn) + ports.append(root.split('_')[-1]) + if len(ports): + portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports)) + + sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n" + % (pidfile,portstr)) + return 1 try: - PRServerConnection(host, port).terminate() + PRServerConnection(ip, port).terminate() except: logger.critical("Stop PRService %s:%d failed" % (host,port)) diff --git a/yocto-poky/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py b/yocto-poky/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py index a9909b8e1..b5cf5591f 100644 --- a/yocto-poky/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py +++ b/yocto-poky/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py @@ -23,9 +23,11 @@ import os import sys import re +import shutil from django.db import transaction from django.db.models import Q from bldcontrol.models import BuildEnvironment, BRLayer, BRVariable, BRTarget, BRBitbake +from orm.models import CustomImageRecipe, Layer, Layer_Version, ProjectLayer import subprocess from toastermain import settings @@ -179,15 +181,9 @@ class LocalhostBEController(BuildEnvironmentController): logger.debug("localhostbecontroller: Stopped bitbake server") def getGitCloneDirectory(self, url, branch): - """ Utility that returns the last component of a git path as directory - """ - import re - components = re.split(r'[:\.\/]', url) - base = components[-2] if components[-1] == "git" else components[-1] - + """Construct unique clone directory name out of url and branch.""" if branch != "HEAD": - return "_%s_%s.toaster_cloned" % (base, branch) - + 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 @@ -197,7 +193,7 @@ class LocalhostBEController(BuildEnvironmentController): return local_checkout_path - def setLayers(self, bitbakes, layers): + def setLayers(self, bitbakes, layers, targets): """ a word of attention: by convention, the first layer for any build will be poky! """ assert self.be.sourcedir is not None @@ -222,23 +218,26 @@ class LocalhostBEController(BuildEnvironmentController): logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos)) - # 2. find checked-out git repos in the sourcedir directory that may help faster cloning + # 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 = {} - for ldir in os.listdir(self.be.sourcedir): - fldir = os.path.join(self.be.sourcedir, ldir) - if os.path.isdir(fldir): + + try: + for remotes in self._shellcmd("git remote -v", self.be.sourcedir).split("\n"): try: - for line in self._shellcmd("git remote -v", fldir).split("\n"): - try: - remote = line.split("\t")[1].split(" ")[0] - if remote not in cached_layers: - cached_layers[remote] = fldir - except IndexError: - pass - except ShellCmdException: - # ignore any errors in collecting git remotes + 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) layerlist = [] @@ -260,13 +259,14 @@ class LocalhostBEController(BuildEnvironmentController): self._shellcmd("git remote remove origin", localdirname) self._shellcmd("git remote add origin \"%s\"" % giturl, localdirname) else: - logger.debug("localhostbecontroller: cloning %s:%s in %s" % (giturl, commit, localdirname)) - self._shellcmd("git clone \"%s\" --single-branch --branch \"%s\" \"%s\"" % (giturl, commit, localdirname)) + logger.debug("localhostbecontroller: cloning %s in %s" % (giturl, localdirname)) + self._shellcmd('git clone "%s" "%s"' % (giturl, localdirname)) # branch magic name "HEAD" will inhibit checkout if commit != "HEAD": logger.debug("localhostbecontroller: checking out commit %s to %s " % (commit, localdirname)) - self._shellcmd("git fetch --all && git checkout \"%s\" && git rebase \"origin/%s\"" % (commit, commit) , localdirname) + ref = commit if re.match('^[a-fA-F0-9]+$', commit) else 'origin/%s' % commit + self._shellcmd('git fetch --all && git reset --hard "%s"' % ref, localdirname) # 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")): @@ -299,6 +299,51 @@ class LocalhostBEController(BuildEnvironmentController): if not os.path.exists(bblayerconf): raise BuildSetupException("BE is not consistent: bblayers.conf file missing at %s" % bblayerconf) + # 6. create custom layer and add custom recipes to it + layerpath = os.path.join(self.be.sourcedir, "_meta-toaster-custom") + if os.path.isdir(layerpath): + shutil.rmtree(layerpath) # remove leftovers from previous builds + for target in targets: + try: + customrecipe = CustomImageRecipe.objects.get(name=target.target, + project=bitbakes[0].req.project) + except CustomImageRecipe.DoesNotExist: + continue # not a custom recipe, skip + + # 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.oonf + 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') + + # create recipe + recipe = os.path.join(layerpath, "recipes", "%s.bb" % target.target) + with open(recipe, "w") as recipef: + recipef.write("require %s\n" % customrecipe.base_recipe.recipe.file_path) + packages = [pkg.name for pkg in customrecipe.packages.all()] + if packages: + recipef.write('IMAGE_INSTALL = "%s"\n' % ' '.join(packages)) + + # create *Layer* objects needed for build machinery to work + layer = Layer.objects.get_or_create(name="Toaster Custom layer", + summary="Layer for custom recipes", + vcs_url="file://%s" % layerpath)[0] + breq = target.req + lver = Layer_Version.objects.get_or_create(project=breq.project, layer=layer, + dirpath=layerpath, build=breq.build)[0] + ProjectLayer.objects.get_or_create(project=breq.project, layercommit=lver, + optional=False) + BRLayer.objects.get_or_create(req=breq, name=layer.name, dirpath=layerpath, + giturl="file://%s" % layerpath) + if os.path.isdir(layerpath): + layerlist.append(layerpath) + BuildEnvironmentController._updateBBLayers(bblayerconf, layerlist) self.islayerset = True @@ -316,7 +361,7 @@ class LocalhostBEController(BuildEnvironmentController): def triggerBuild(self, bitbake, layers, variables, targets): # set up the buid environment with the needed layers - self.setLayers(bitbake, layers) + self.setLayers(bitbake, layers, targets) self.writeConfFile("conf/toaster-pre.conf", variables) self.writeConfFile("conf/toaster.conf", raw = "INHERIT+=\"toaster buildhistory\"") diff --git a/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py b/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py index b2c573c9e..5e70437b2 100644 --- a/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py +++ b/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/checksettings.py @@ -4,7 +4,7 @@ from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdExcep from bldcontrol.models import BuildRequest, BuildEnvironment, BRError from orm.models import ToasterSetting, Build import os -import sys, traceback +import traceback def DN(path): if path is None: @@ -21,7 +21,7 @@ class Command(NoArgsCommand): super(Command, self).__init__(*args, **kwargs) self.guesspath = DN(DN(DN(DN(DN(DN(DN(__file__))))))) - def _find_first_path_for_file(self, startdirectory, filename, level = 0): + def _find_first_path_for_file(self, startdirectory, filename, level=0): if level < 0: return None dirs = [] @@ -38,7 +38,7 @@ class Command(NoArgsCommand): return ret return None - def _recursive_list_directories(self, startdirectory, level = 0): + def _recursive_list_directories(self, startdirectory, level=0): if level < 0: return [] dirs = [] @@ -50,49 +50,23 @@ class Command(NoArgsCommand): except OSError: pass for j in dirs: - dirs = dirs + self._recursive_list_directories(j, level - 1) + dirs = dirs + self._recursive_list_directories(j, level - 1) return dirs - def _get_suggested_sourcedir(self, be): - if be.betype != BuildEnvironment.TYPE_LOCAL: - return "" - return DN(DN(DN(self._find_first_path_for_file(self.guesspath, "toasterconf.json", 4)))) - - def _get_suggested_builddir(self, be): - if be.betype != BuildEnvironment.TYPE_LOCAL: - return "" - return DN(self._find_first_path_for_file(DN(self.guesspath), "bblayers.conf", 4)) - def _verify_build_environment(self): - # refuse to start if we have no build environments - while BuildEnvironment.objects.count() == 0: - print(" !! No build environments found. Toaster needs at least one build environment in order to be able to run builds.\n" + - "You can manually define build environments in the database table bldcontrol_buildenvironment.\n" + - "Or Toaster can define a simple localhost-based build environment for you.") - - i = raw_input(" -- Do you want to create a basic localhost build environment ? (Y/n) "); - if not len(i) or i.startswith("y") or i.startswith("Y"): - BuildEnvironment.objects.create(pk = 1, betype = 0) - else: - raise Exception("Toaster cannot start without build environments. Aborting.") - + # 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 - print("\nVerifying the build environment. If the local build environment is not properly configured, you will be asked to configure it.") def _update_sourcedir(): - suggesteddir = self._get_suggested_sourcedir(be) - if len(suggesteddir) > 0: - be.sourcedir = raw_input("This is the directory Toaster uses to check out the source code of the layers you will build. Toaster will create new clones of the layers, so existing content in the chosen directory will not be changed.\nToaster suggests you use \"%s\" as your layers checkout directory. If you select this directory, a layer like \"meta-intel\" will end up in \"%s/meta-intel\".\nPress Enter to select \"%s\" or type the full path to a different directory. If you provide your own directory, it must be a parent of the cloned directory for the sources you are using to run Toaster: " % (suggesteddir, suggesteddir, suggesteddir)) - else: - be.sourcedir = raw_input("Toaster needs to know in which directory it should check out the source code of the layers you will build. The directory should be a parent of the cloned directory for the sources you are using to run Toaster. Toaster will create new clones of the layers, so existing content in the chosen directory will not be changed.\nType the full path to the directory (for example: \"%s\": " % os.environ.get('HOME', '/tmp/')) - if len(be.sourcedir) == 0 and len(suggesteddir) > 0: - be.sourcedir = suggesteddir + be.sourcedir = os.environ.get('TOASTER_DIR') return True if len(be.sourcedir) == 0: @@ -103,23 +77,13 @@ class Command(NoArgsCommand): print "\n -- Validation: The layers checkout directory must be set to an absolute path." is_changed = _update_sourcedir() - if not be.sourcedir in DN(__file__): - print "\n -- Validation: The layers checkout directory must be a parent of the current checkout." - is_changed = _update_sourcedir() - if is_changed: if be.betype == BuildEnvironment.TYPE_LOCAL: be.needs_import = True return True def _update_builddir(): - suggesteddir = self._get_suggested_builddir(be) - if len(suggesteddir) > 0: - be.builddir = raw_input("Toaster needs to know where your build directory is located.\nThe build directory is where all the artifacts created by your builds will be stored. Toaster suggests \"%s\".\nPress Enter to select \"%s\" or type the full path to a different directory: " % (suggesteddir, suggesteddir)) - else: - be.builddir = raw_input("Toaster needs to know where is your build directory.\nThe build directory is where all the artifacts created by your builds will be stored. Type the full path to the directory (for example: \" %s/build\")" % os.environ.get('HOME','/tmp/')) - if len(be.builddir) == 0 and len(suggesteddir) > 0: - be.builddir = suggesteddir + be.builddir = os.environ.get('TOASTER_DIR')+"/build" return True if len(be.builddir) == 0: @@ -138,79 +102,51 @@ class Command(NoArgsCommand): if be.needs_import: - print "\nToaster can use a SINGLE predefined configuration file to set up default project settings and layer information sources.\n" - - # find configuration files - config_files = [] - for dirname in self._recursive_list_directories(be.sourcedir,2): - if os.path.exists(os.path.join(dirname, ".templateconf")): - import subprocess - proc = subprocess.Popen('bash -c ". '+os.path.join(dirname, ".templateconf")+'; echo \"\$TEMPLATECONF\""', shell=True, stdout=subprocess.PIPE) - conffilepath, stderroroutput = proc.communicate() - proc.wait() - if proc.returncode != 0: - raise Exception("Failed to source TEMPLATECONF: %s" % stderroroutput) - - conffilepath = os.path.join(conffilepath.strip(), "toasterconf.json") - candidatefilepath = os.path.join(dirname, conffilepath) - if "toaster_cloned" in candidatefilepath: - continue - if os.path.exists(candidatefilepath): - config_files.append(candidatefilepath) - - if len(config_files) > 0: - print "Toaster will list now the configuration files that it found. Select the number to use the desired configuration file." - for cf in config_files: - print " [%d] - %s" % (config_files.index(cf) + 1, cf) - print "\n [0] - Exit without importing any file" - try: - i = raw_input("\nEnter your option: ") - if len(i) and (int(i) - 1 >= 0 and int(i) - 1 < len(config_files)): - print "\nImporting file: %s" % config_files[int(i)-1] - from loadconf import Command as LoadConfigCommand - - LoadConfigCommand()._import_layer_config(config_files[int(i)-1]) - # we run lsupdates after config update - print "\nLayer configuration imported. Updating information from the layer sources, please wait.\nYou can re-update any time later by running bitbake/lib/toaster/manage.py lsupdates" - from django.core.management import call_command - call_command("lsupdates") - - # we don't look for any other config files - return is_changed - except Exception as e: - print "Failure while trying to import the toaster config file: %s" % e - traceback.print_exc(e) - else: - print "\nToaster could not find a configuration file. You need to configure Toaster manually using the web interface, or create a configuration file and use\n bitbake/lib/toaster/managepy.py loadconf [filename]\n command to load it. You can use https://wiki.yoctoproject.org/wiki/File:Toasterconf.json.txt.patch as a starting point." - - - + try: + config_file = os.environ.get('TOASTER_CONF') + print "\nImporting file: %s" % config_file + from loadconf import Command as LoadConfigCommand + + LoadConfigCommand()._import_layer_config(config_file) + # we run lsupdates after config update + print "\nLayer configuration imported. Updating information from the layer sources, please wait.\nYou can re-update any time later by running bitbake/lib/toaster/manage.py lsupdates" + from django.core.management import call_command + call_command("lsupdates") + + # we don't look for any other config files + return is_changed + except Exception as e: + print "Failure while trying to import the toaster config file %s: %s" %\ + (config_file, e) + traceback.print_exc(e) return is_changed - while (_verify_be()): + 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 = '') + 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") + 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) + BuildRequest.objects.filter(state=BuildRequest.REQ_INPROGRESS).update(state=BuildRequest.REQ_FAILED) - BuildEnvironment.objects.update(lock = BuildEnvironment.LOCK_FREE) + 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()) + Build.objects.filter(outcome=Build.IN_PROGRESS).update(outcome=Build.FAILED, completed_on=timezone.now()) return 0 diff --git a/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py b/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py index 718e1441d..48dc618bc 100644 --- a/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py +++ b/yocto-poky/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py @@ -5,6 +5,7 @@ from bldcontrol.bbcontroller import getBuildEnvironmentController, ShellCmdExcep from bldcontrol.models import BuildRequest, BuildEnvironment, BRError, BRVariable import os import logging +import time logger = logging.getLogger("ToasterScheduler") @@ -118,7 +119,7 @@ class Command(NoArgsCommand): br.save() # transpose target information for brtarget in br.brtarget_set.all(): - Target.objects.create(build = br.build, target= brtarget.target) + 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) @@ -128,6 +129,12 @@ class Command(NoArgsCommand): def handle_noargs(self, **options): - self.cleanup() - self.archive() - self.schedule() + while True: + try: + self.cleanup() + self.archive() + self.schedule() + except: + pass + + time.sleep(1) diff --git a/yocto-poky/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py b/yocto-poky/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py new file mode 100644 index 000000000..9b50bc1c0 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/bldcontrol/migrations/0009_auto__add_field_brlayer_layer_version.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'BRLayer.layer_version' + db.add_column(u'bldcontrol_brlayer', 'layer_version', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Layer_Version'], null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'BRLayer.layer_version' + db.delete_column(u'bldcontrol_brlayer', 'layer_version_id') + + + models = { + u'bldcontrol.brbitbake': { + 'Meta': {'object_name': 'BRBitbake'}, + 'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']", 'unique': 'True'}) + }, + u'bldcontrol.brerror': { + 'Meta': {'object_name': 'BRError'}, + 'errmsg': ('django.db.models.fields.TextField', [], {}), + 'errtype': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), + 'traceback': ('django.db.models.fields.TextField', [], {}) + }, + u'bldcontrol.brlayer': { + 'Meta': {'object_name': 'BRLayer'}, + 'commit': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'giturl': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}) + }, + u'bldcontrol.brtarget': { + 'Meta': {'object_name': 'BRTarget'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + u'bldcontrol.brvariable': { + 'Meta': {'object_name': 'BRVariable'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'req': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildRequest']"}), + 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'bldcontrol.buildenvironment': { + 'Meta': {'object_name': 'BuildEnvironment'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'bbaddress': ('django.db.models.fields.CharField', [], {'max_length': '254', 'blank': 'True'}), + 'bbport': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'bbstate': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'bbtoken': ('django.db.models.fields.CharField', [], {'max_length': '126', 'blank': 'True'}), + 'betype': ('django.db.models.fields.IntegerField', [], {}), + 'builddir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lock': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'sourcedir': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'bldcontrol.buildrequest': { + 'Meta': {'object_name': 'BuildRequest'}, + 'build': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['orm.Build']", 'unique': 'True', 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'environment': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['bldcontrol.BuildEnvironment']", 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'orm.bitbakeversion': { + 'Meta': {'object_name': 'BitbakeVersion'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + u'orm.branch': { + 'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.build': { + 'Meta': {'object_name': 'Build'}, + 'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'completed_on': ('django.db.models.fields.DateTimeField', [], {}), + 'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'started_on': ('django.db.models.fields.DateTimeField', [], {}) + }, + u'orm.layer': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'}, + 'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}) + }, + u'orm.layer_version': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}), + 'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}), + 'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.layersource': { + 'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'}, + 'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}), + 'sourcetype': ('django.db.models.fields.IntegerField', [], {}) + }, + u'orm.project': { + 'Meta': {'object_name': 'Project'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) + }, + u'orm.release': { + 'Meta': {'object_name': 'Release'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}), + 'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + } + } + + complete_apps = ['bldcontrol']
\ No newline at end of file diff --git a/yocto-poky/bitbake/lib/toaster/bldcontrol/models.py b/yocto-poky/bitbake/lib/toaster/bldcontrol/models.py index b61de58a3..ab4110530 100644 --- a/yocto-poky/bitbake/lib/toaster/bldcontrol/models.py +++ b/yocto-poky/bitbake/lib/toaster/bldcontrol/models.py @@ -1,6 +1,6 @@ from django.db import models from django.core.validators import MaxValueValidator, MinValueValidator -from orm.models import Project, ProjectLayer, ProjectVariable, ProjectTarget, Build +from orm.models import Project, ProjectLayer, ProjectVariable, ProjectTarget, Build, Layer_Version # a BuildEnvironment is the equivalent of the "build/" directory on the localhost class BuildEnvironment(models.Model): @@ -39,40 +39,6 @@ class BuildEnvironment(models.Model): created = models.DateTimeField(auto_now_add = True) updated = models.DateTimeField(auto_now = True) - - def get_artifact_type(self, path): - if self.betype == BuildEnvironment.TYPE_LOCAL: - try: - import magic - - # fair warning: this is a mess; there are multiple competeing and incompatible - # magic modules floating around, so we try some of the most common combinations - - try: # we try ubuntu's python-magic 5.4 - m = magic.open(magic.MAGIC_MIME_TYPE) - m.load() - return m.file(path) - except AttributeError: - pass - - try: # we try python-magic 0.4.6 - m = magic.Magic(magic.MAGIC_MIME) - return m.from_file(path) - except AttributeError: - pass - - try: # we try pip filemagic 1.6 - m = magic.Magic(flags=magic.MAGIC_MIME_TYPE) - return m.id_filename(path) - except AttributeError: - pass - - return "binary/octet-stream" - except ImportError: - return "binary/octet-stream" - raise Exception("FIXME: artifact type not implemented for build environment type %s" % self.get_betype_display()) - - def get_artifact(self, path): if self.betype == BuildEnvironment.TYPE_LOCAL: return open(path, "r") @@ -137,6 +103,7 @@ class BRLayer(models.Model): giturl = models.CharField(max_length = 254) commit = models.CharField(max_length = 254) dirpath = models.CharField(max_length = 254) + layer_version = models.ForeignKey(Layer_Version, null=True) class BRBitbake(models.Model): req = models.ForeignKey(BuildRequest, unique = True) # only one bitbake for a request diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py index 2a2078f08..eac167b2e 100755 --- a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_automation_test.py @@ -221,6 +221,68 @@ def extract_number_from_string(s): """ return re.findall(r'([0-9]+)', s) +# Below is decorator derived from toaster backend test code +class NoParsingFilter(logging.Filter): + def filter(self, record): + return record.levelno == 100 + +def LogResults(original_class): + orig_method = original_class.run + + #rewrite the run method of unittest.TestCase to add testcase logging + def run(self, result, *args, **kws): + orig_method(self, result, *args, **kws) + passed = True + testMethod = getattr(self, self._testMethodName) + + #if test case is decorated then use it's number, else use it's name + try: + test_case = testMethod.test_case + except AttributeError: + test_case = self._testMethodName + + #create custom logging level for filtering. + custom_log_level = 100 + logging.addLevelName(custom_log_level, 'RESULTS') + caller = os.path.basename(sys.argv[0]) + + def results(self, message, *args, **kws): + if self.isEnabledFor(custom_log_level): + self.log(custom_log_level, message, *args, **kws) + logging.Logger.results = results + + logging.basicConfig(filename=os.path.join(os.getcwd(),'results-'+caller+'.log'), + filemode='w', + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%H:%M:%S', + level=custom_log_level) + for handler in logging.root.handlers: + handler.addFilter(NoParsingFilter()) +# local_log = logging.getLogger(caller) + local_log = logging.getLogger() + + #check status of tests and record it + for (name, msg) in result.errors: + if self._testMethodName == str(name).split(' ')[0]: + local_log.results("Testcase "+str(test_case)+": ERROR") + local_log.results("Testcase "+str(test_case)+":\n"+msg+"\n\n\n") + passed = False + for (name, msg) in result.failures: + if self._testMethodName == str(name).split(' ')[0]: + local_log.results("Testcase "+str(test_case)+": FAILED") + local_log.results("Testcase "+str(test_case)+":\n"+msg+"\n\n\n") + passed = False + for (name, msg) in result.skipped: + if self._testMethodName == str(name).split(' ')[0]: + local_log.results("Testcase "+str(test_case)+": SKIPPED"+"\n\n\n") + passed = False + if passed: + local_log.results("Testcase "+str(test_case)+": PASSED"+"\n\n\n") + + original_class.run = run + return original_class + + ########################################### @@ -321,7 +383,10 @@ class toaster_cases_base(unittest.TestCase): log_path = log_dir + os.sep + self.browser + '-' +\ item + '-' + add_name + '-' + str(self.screenshot_sequence) + '.png' if item == 'native': - os.system("scrot " + log_path) + if self.host_os == "linux": + os.system("scrot " + log_path) + elif self.host_os=="darwin": + os.system("screencapture -x " + log_path) elif item == 'selenium': self.driver.get_screenshot_as_file(log_path) self.screenshot_sequence += 1 @@ -531,6 +596,7 @@ class toaster_cases_base(unittest.TestCase): def is_text_present (self, patterns): for pattern in patterns: if str(pattern) not in self.driver.page_source: + print pattern return False return True @@ -592,7 +658,7 @@ class toaster_cases_base(unittest.TestCase): # Note: to comply with the unittest framework, we call these test_xxx functions # from run_toastercases.py to avoid calling setUp() and tearDown() multiple times - +@LogResults class toaster_cases(toaster_cases_base): ############## # CASE 901 # @@ -627,12 +693,12 @@ class toaster_cases(toaster_cases_base): if is_list_inverted(column_list): self.driver.find_element_by_link_text(key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key)) else: - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list), msg=("%s column not sequenced" % key)) self.driver.find_element_by_link_text(key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_inverted(column_list)) + self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key)) self.log.info("case passed") @@ -656,10 +722,10 @@ class toaster_cases(toaster_cases_base): # if nothing found, we still count it as "pass" if new_target_column_texts: for text in new_target_column_texts: - self.failUnless(text.find(pattern)) + self.assertTrue(text.find(pattern), msg=("%s item doesn't exist " % pattern)) self.driver.find_element_by_css_selector("i.icon-remove").click() target_column_texts = self.get_table_column_text("class", "target") - self.failUnless(ori_target_column_texts == target_column_texts) + self.assertTrue(ori_target_column_texts == target_column_texts, msg=("builds changed after operations")) ############## @@ -682,10 +748,9 @@ class toaster_cases(toaster_cases_base): try: temp_element = self.find_element_by_text_in_table('otable', item) # this is how we find "filter icon" in the same level as temp_element(where "a" means clickable, "i" means icon) - self.failUnless(temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']")) + self.assertTrue(temp_element.find_element_by_xpath("..//*/a/i[@class='icon-filter filtered']")) except Exception,e: - self.log.error(" %s cannot be found! %s" % (item, e)) - self.failIf(True) + self.assertFalse(True, msg=(" %s cannot be found! %s" % (item, e))) raise # step 5-6 temp_element = self.find_element_by_link_text_in_table('otable', 'Outcome') @@ -728,10 +793,12 @@ class toaster_cases(toaster_cases_base): # This is how we find the "default" rows-number! rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text) print rows_displayed - self.failUnless(self.get_table_element(self.table_name, rows_displayed)) - self.failIf(self.get_table_element(self.table_name, rows_displayed + 1)) + self.assertTrue(self.get_table_element(self.table_name, rows_displayed), msg=("not enough rows displayed")) + self.assertFalse(self.get_table_element(self.table_name, rows_displayed + 1), \ + msg=("more rows displayed than expected")) # Search text box background text is "Search tasks" - self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search tasks']")) + self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search tasks']"),\ + msg=("background text doesn't exist")) self.driver.find_element_by_id("search").clear() self.driver.find_element_by_id("search").send_keys("busybox") @@ -760,22 +827,23 @@ class toaster_cases(toaster_cases_base): column_list = self.get_table_column_text("class", table_head_dict[key]) # after 1st click, the list should be either sequenced or inverted, but we don't have a "default order" here # the point is, after another click, it should be another order -# the fist case is special:this means every item in column_list is the same, so +# the first case is special:this means every item in column_list is the same, so # after one click, either sequenced or inverted will be fine if (is_list_inverted(column_list) and is_list_sequenced(column_list)) \ or (not column_list) : self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list), \ + msg=("%s column not in any order" % key)) elif is_list_inverted(column_list): self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key)) else: - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list), msg=("%s column not in order" % key)) self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_inverted(column_list)) + self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key)) # step 8-10 # filter dict: {link text name : filter table name in xpath} filter_dict = {'Executed':'filter_executed', 'Outcome':'filter_outcome', 'Cache attempt':'filter_cache_attempt'} @@ -834,9 +902,9 @@ class toaster_cases(toaster_cases_base): self.find_element_by_link_text_in_table('nav', key).click() head_list = self.get_table_head_text('otable') for item in test_dict[key]['check_head_list']: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("%s not in head row" % item)) column_list = self.get_table_column_text('class', test_dict[key]['class']) - self.failUnless(is_list_inverted(column_list)) + self.assertTrue(is_list_inverted(column_list), msg=("%s column not inverted" % key)) self.driver.find_element_by_id("edit-columns-button").click() for item2 in test_dict[key]['check_column_list']: @@ -862,21 +930,24 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_partial_link_text("Generated files").click() head_list = self.get_table_head_text('otable') for item in ['File', 'Size']: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("%s not in head row" % item)) c_list = self.get_table_column_text('class', 'path') - self.failUnless(is_list_sequenced(c_list)) + self.assertTrue(is_list_sequenced(c_list), msg=("column not in order")) # step 7 self.driver.find_element_by_partial_link_text("Runtime dependencies").click() # save sceen here to observe... # note that here table name is not 'otable' head_list = self.get_table_head_text('dependencies') for item in ['Package', 'Version', 'Size']: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("%s not in head row" % item)) c_list = self.get_table_column_text_by_column_number('dependencies', 1) - self.failUnless(is_list_sequenced(c_list)) + self.assertTrue(is_list_sequenced(c_list), msg=("list not in order")) texts = ['Size', 'License', 'Recipe', 'Recipe version', 'Layer', \ - 'Layer branch', 'Layer commit', 'Layer directory'] - self.failUnless(self.is_text_present(texts)) + 'Layer branch', 'Layer commit'] + time.sleep(1) +# for text in texts: +# self.assertTrue(self.is_text_present(text), msg=("text %s not in page" % text)) + self.assertTrue(self.is_text_present(texts), msg=("text not in page")) ############## @@ -898,8 +969,8 @@ class toaster_cases(toaster_cases_base): # This is how we find the "default" rows-number! rows_displayed = int(Select(self.driver.find_element_by_css_selector("select.pagesize")).first_selected_option.text) print rows_displayed - self.failUnless(self.get_table_element(self.table_name, rows_displayed)) - self.failIf(self.get_table_element(self.table_name, rows_displayed + 1)) + self.assertTrue(self.get_table_element(self.table_name, rows_displayed)) + self.assertFalse(self.get_table_element(self.table_name, rows_displayed + 1)) # Check the default table is sorted by Recipe tasks_column_count = len(self.driver.find_elements_by_xpath("/html/body/div[2]/div/div[2]/div[2]/table/tbody/tr/td[1]")) @@ -907,10 +978,10 @@ class toaster_cases(toaster_cases_base): default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1) #print default_column_list - self.failUnless(is_list_sequenced(default_column_list)) + self.assertTrue(is_list_sequenced(default_column_list)) # Search text box background text is "Search recipes" - self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) self.driver.find_element_by_id("search").clear() self.driver.find_element_by_id("search").send_keys(test_package1) @@ -937,7 +1008,7 @@ class toaster_cases(toaster_cases_base): #self.driver.find_element_by_partial_link_text("zlib").click() #self.driver.back() - #self.failUnless(is_list_inverted(inverted_column_list)) + #self.assertTrue(is_list_inverted(inverted_column_list)) #self.find_element_by_link_text_in_table(self.table_name, 'Recipe').click() table_head_dict = {'Recipe':'recipe__name', 'Recipe file':'recipe_file', 'Section':'recipe_section', \ @@ -950,52 +1021,52 @@ class toaster_cases(toaster_cases_base): or (not column_list) : self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list)) self.driver.find_element_by_partial_link_text("acl").click() self.driver.back() - self.failUnless(is_list_sequenced(column_list) or is_list_inverted(column_list)) + self.assertTrue(is_list_sequenced(column_list) or is_list_inverted(column_list)) # Search text box background text is "Search recipes" - self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) self.driver.find_element_by_id("search").clear() self.driver.find_element_by_id("search").send_keys(test_package2) self.driver.find_element_by_id("search-button").click() column_search_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_search_list) or is_list_inverted(column_search_list)) + self.assertTrue(is_list_sequenced(column_search_list) or is_list_inverted(column_search_list)) self.driver.find_element_by_css_selector("i.icon-remove").click() elif is_list_inverted(column_list): self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list)) self.driver.find_element_by_partial_link_text("acl").click() self.driver.back() - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list)) # Search text box background text is "Search recipes" - self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) self.driver.find_element_by_id("search").clear() self.driver.find_element_by_id("search").send_keys(test_package2) self.driver.find_element_by_id("search-button").click() column_search_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_sequenced(column_search_list)) + self.assertTrue(is_list_sequenced(column_search_list)) self.driver.find_element_by_css_selector("i.icon-remove").click() else: - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list)) self.find_element_by_link_text_in_table(self.table_name, key).click() column_list = self.get_table_column_text("class", table_head_dict[key]) - self.failUnless(is_list_inverted(column_list)) + self.assertTrue(is_list_inverted(column_list)) try: self.driver.find_element_by_partial_link_text("acl").click() except: self.driver.find_element_by_partial_link_text("zlib").click() self.driver.back() - self.failUnless(is_list_inverted(column_list)) + self.assertTrue(is_list_inverted(column_list)) # Search text box background text is "Search recipes" - self.failUnless(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) + self.assertTrue(self.driver.find_element_by_xpath("//*[@id='searchform']/*[@placeholder='Search recipes']")) self.driver.find_element_by_id("search").clear() self.driver.find_element_by_id("search").send_keys(test_package2) self.driver.find_element_by_id("search-button").click() column_search_list = self.get_table_column_text("class", table_head_dict[key]) #print column_search_list - self.failUnless(is_list_inverted(column_search_list)) + self.assertTrue(is_list_inverted(column_search_list)) self.driver.find_element_by_css_selector("i.icon-remove").click() # Bug 5919 @@ -1011,7 +1082,7 @@ class toaster_cases(toaster_cases_base): #print tasks_column_count default_column_list = self.get_table_column_text_by_column_number(self.table_name, 1) #print default_column_list - self.failUnless(is_list_sequenced(default_column_list)) + self.assertTrue(is_list_sequenced(default_column_list)) self.driver.find_element_by_id("edit-columns-button").click() self.driver.find_element_by_id("recipe_file").click() @@ -1061,31 +1132,31 @@ class toaster_cases(toaster_cases_base): # step 3 head_list = self.get_table_head_text('otable') for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']: - self.failUnless(item in head_list) - self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) + self.driver.find_element_by_id("edit-columns-button").click() self.driver.find_element_by_id("depends_on").click() self.driver.find_element_by_id("layer_version__branch").click() self.driver.find_element_by_id("layer_version__layer__commit").click() self.driver.find_element_by_id("depends_by").click() - self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.driver.find_element_by_id("edit-columns-button").click() # check if columns selected above is shown - check_list = ['Dependencies', 'Layer branch', 'Layer commit', 'Layer directory', 'Reverse dependencies'] + check_list = ['Dependencies', 'Layer branch', 'Layer commit', 'Reverse dependencies'] head_list = self.get_table_head_text('otable') time.sleep(2) print head_list for item in check_list: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) # un-check 'em all - self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.driver.find_element_by_id("edit-columns-button").click() self.driver.find_element_by_id("depends_on").click() self.driver.find_element_by_id("layer_version__branch").click() self.driver.find_element_by_id("layer_version__layer__commit").click() self.driver.find_element_by_id("depends_by").click() - self.driver.find_element_by_css_selector("button.btn.dropdown-toggle").click() + self.driver.find_element_by_id("edit-columns-button").click() # don't exist any more head_list = self.get_table_head_text('otable') for item in check_list: - self.failIf(item in head_list) + self.assertFalse(item in head_list, msg=("item %s should not be in head row" % item)) ############## @@ -1101,7 +1172,7 @@ class toaster_cases(toaster_cases_base): # step 3 head_list = self.get_table_head_text('otable') for item in ['Recipe', 'Recipe version', 'Recipe file', 'Section', 'License', 'Layer']: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) # step 4 self.driver.find_element_by_id("edit-columns-button").click() # save screen @@ -1315,7 +1386,8 @@ class toaster_cases(toaster_cases_base): head_list = self.get_table_head_text('otable') print head_list print len(head_list) - self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) + self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \ + msg=("head row contents wrong")) # step 8 # search other string. and click "Variable" to re-sort, check if table # head is still the same @@ -1324,10 +1396,12 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("search-button").click() self.find_element_by_link_text_in_table('otable', 'Variable').click() head_list = self.get_table_head_text('otable') - self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) + self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \ + msg=("head row contents wrong")) self.find_element_by_link_text_in_table('otable', 'Variable').click() head_list = self.get_table_head_text('otable') - self.failUnless(head_list == ['Variable', 'Value', 'Set in file', 'Description']) + self.assertTrue(head_list == ['Variable', 'Value', 'Set in file', 'Description'], \ + msg=("head row contents wrong")) ############## @@ -1343,11 +1417,11 @@ class toaster_cases(toaster_cases_base): self.find_element_by_link_text_in_table('nav', 'Configuration').click() self.driver.find_element_by_link_text("BitBake variables").click() variable_list = self.get_table_column_text('class', 'variable_name') - self.failUnless(is_list_sequenced(variable_list)) + self.assertTrue(is_list_sequenced(variable_list), msg=("list not in order")) # step 4 self.find_element_by_link_text_in_table('otable', 'Variable').click() variable_list = self.get_table_column_text('class', 'variable_name') - self.failUnless(is_list_inverted(variable_list)) + self.assertTrue(is_list_inverted(variable_list), msg=("list not inverted")) self.find_element_by_link_text_in_table('otable', 'Variable').click() # step 5 # searching won't change the sequentiality @@ -1355,7 +1429,7 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("search").send_keys("lib") self.driver.find_element_by_id("search-button").click() variable_list = self.get_table_column_text('class', 'variable_name') - self.failUnless(is_list_sequenced(variable_list)) + self.assertTrue(is_list_sequenced(variable_list), msg=("list not in order")) ############## @@ -1369,7 +1443,7 @@ class toaster_cases(toaster_cases_base): # Step 2 # default sequence in "Completed on" column is inverted c_list = self.get_table_column_text('class', 'completed_on') - self.failUnless(is_list_inverted(c_list)) + self.assertTrue(is_list_inverted(c_list), msg=("list not inverted")) # step 3 self.driver.find_element_by_id("edit-columns-button").click() self.driver.find_element_by_id("started_on").click() @@ -1377,8 +1451,8 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("time").click() self.driver.find_element_by_id("edit-columns-button").click() head_list = self.get_table_head_text('otable') - for item in ['Outcome', 'Target', 'Machine', 'Started on', 'Completed on', 'Failed tasks', 'Errors', 'Warnings', 'Warnings', 'Time']: - self.failUnless(item in head_list) + for item in ['Outcome', 'Recipe', 'Machine', 'Started on', 'Completed on', 'Failed tasks', 'Errors', 'Warnings', 'Warnings', 'Time']: + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) ############## @@ -1392,7 +1466,7 @@ class toaster_cases(toaster_cases_base): # Please refer to case 924 requirement # default sequence in "Completed on" column is inverted c_list = self.get_table_column_text('class', 'completed_on') - self.failUnless(is_list_inverted(c_list)) + self.assertTrue(is_list_inverted(c_list), msg=("list not inverted")) # Step 4 # click Errors , order in "Completed on" should be disturbed. Then hide # error column to check if order in "Completed on" can be restored @@ -1403,7 +1477,7 @@ class toaster_cases(toaster_cases_base): # Note: without time.sleep here, there'll be unpredictable error..TBD time.sleep(1) c_list = self.get_table_column_text('class', 'completed_on') - self.failUnless(is_list_inverted(c_list)) + self.assertTrue(is_list_inverted(c_list), msg=("list not inverted")) ############## @@ -1419,7 +1493,7 @@ class toaster_cases(toaster_cases_base): self.find_element_by_link_text_in_table('nav', 'Packages').click() check_head_list = ['Package', 'Package version', 'Size', 'Recipe'] head_list = self.get_table_head_text('otable') - self.failUnless(head_list == check_head_list) + self.assertTrue(head_list == check_head_list, msg=("head row not as expected")) # Step 4 # pulldown menu option_ids = ['recipe__layer_version__layer__name', 'recipe__layer_version__branch', \ @@ -1448,7 +1522,7 @@ class toaster_cases(toaster_cases_base): self.find_element_by_link_text_in_table('nav', 'Packages').click() # column -- Package column_list = self.get_table_column_text_by_column_number('otable', 1) - self.failUnless(is_list_sequenced(column_list)) + self.assertTrue(is_list_sequenced(column_list), msg=("list not in order")) self.find_element_by_link_text_in_table('otable', 'Size').click() @@ -1470,7 +1544,7 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("edit-columns-button").click() #get modified table header new_head = self.get_table_head_text('otable') - self.failUnless(head_list > new_head) + self.assertTrue(head_list > new_head) ############## # CASE 943 # @@ -1487,7 +1561,7 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("search").send_keys("bash") self.driver.find_element_by_id("search-button").click() #check for the search result message "XX packages found" - self.failUnless(self.is_text_present("packages found")) + self.assertTrue(self.is_text_present("packages found"), msg=("no packages found text")) ############## @@ -1508,11 +1582,12 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("edit-columns-button").click() # otable is the recipes table here otable_head_text = self.get_table_head_text('otable') - for item in ["Layer", "Layer branch", "Layer commit", "Layer directory"]: - self.failIf(item not in otable_head_text) + for item in ["Layer", "Layer branch", "Layer commit"]: + self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item)) # click the fist recipe, whatever it is self.get_table_element("otable", 1, 1).click() - self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit", "Recipe file"])) + self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit", "Recipe file"]), \ + msg=("text not in web page")) # step 2: test Packages page stuff. almost same as above self.driver.back() @@ -1525,10 +1600,11 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("edit-columns-button").click() otable_head_text = self.get_table_head_text("otable") for item in ["Layer", "Layer branch", "Layer commit"]: - self.failIf(item not in otable_head_text) + self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item)) # click the fist recipe, whatever it is self.get_table_element("otable", 1, 1).click() - self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"])) + self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit"]), \ + msg=("text not in web page")) # step 3: test Packages core-image-minimal(images) stuff. almost same as above. Note when future element-id changes... self.driver.back() @@ -1540,17 +1616,18 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("edit-columns-button").click() otable_head_text = self.get_table_head_text("otable") for item in ["Layer", "Layer branch", "Layer commit"]: - self.failIf(item not in otable_head_text) + self.assertFalse(item not in otable_head_text, msg=("item %s should be in head row" % item)) # click the fist recipe, whatever it is self.get_table_element("otable", 1, 1).click() - self.failUnless(self.is_text_present(["Layer", "Layer branch", "Layer commit"])) + self.assertTrue(self.is_text_present(["Layer", "Layer branch", "Layer commit"]), \ + msg=("text not in web page")) # step 4: check Configuration page self.driver.back() self.driver.find_element_by_link_text("Configuration").click() otable_head_text = self.get_table_head_text() for item in ["Layer", "Layer branch", "Layer commit"]: - self.failIf(item not in otable_head_text) + self.assertTrue(item not in otable_head_text, msg=("item %s should not be in head row" % item)) ############## @@ -1575,14 +1652,14 @@ class toaster_cases(toaster_cases_base): # Sure we can use driver.get(url) to refresh page, but since page will vary, we use click link text here self.driver.find_element_by_link_text(items).click() Select(self.driver.find_element_by_css_selector("select.pagesize")).select_by_visible_text(str(rows_displayed)) - self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) - self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) + self.assertTrue(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) + self.assertFalse(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) # click 1st package, then go back to check if it's still those rows shown. self.driver.find_element_by_xpath(xpath_table + "/tr[1]/td[1]").click() self.driver.find_element_by_link_text(items).click() - self.failUnless(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) - self.failIf(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) + self.assertTrue(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed) +"]")) + self.assertFalse(self.is_element_present(By.XPATH, xpath_table + "/tr[" + str(rows_displayed+1) +"]")) ############## @@ -1673,7 +1750,7 @@ class toaster_cases(toaster_cases_base): check_list = ['Description', 'Set in file'] head_list = self.get_table_head_text('otable') for item in check_list: - self.failIf(item in head_list) + self.assertFalse(item in head_list, msg=("item %s should not be in head row" % item)) # check these 2 options and verify again self.driver.find_element_by_id('edit-columns-button').click() self.driver.find_element_by_xpath(xpath_option('description')).click() @@ -1681,7 +1758,7 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id('edit-columns-button').click() head_list = self.get_table_head_text('otable') for item in check_list: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) ############## @@ -1703,7 +1780,7 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_id("search-button").click() #get number of variables visible after search number_after_search = self.driver.find_element_by_class_name('page-header').text - self.failUnless(number_before_search > number_after_search) + self.assertTrue(number_before_search > number_after_search, msg=("items should be less after search")) ############## @@ -1722,11 +1799,11 @@ class toaster_cases(toaster_cases_base): self.driver.find_element_by_partial_link_text("Directory structure") except Exception,e: self.log.error(e) - self.failIf(True) + self.assertFalse(True) # step 4 head_list = self.get_table_head_text('otable') for item in ['Package', 'Package version', 'Size', 'Dependencies', 'Reverse dependencies', 'Recipe']: - self.failUnless(item in head_list) + self.assertTrue(item in head_list, msg=("item %s not in head row" % item)) # step 5-6 self.driver.find_element_by_id("edit-columns-button").click() selectable_class = 'checkbox' @@ -1746,22 +1823,15 @@ class toaster_cases(toaster_cases_base): unselectable_list.append(element.text) # check them for item in selectable_check_list: - if item not in selectable_list: - self.log.error(" %s not found in dropdown menu \n" % item) - self.failIf(True) + self.assertTrue(item in selectable_list, msg=("%s not found in dropdown menu" % item)) for item in unselectable_check_list: - if item not in unselectable_list: - self.log.error(" %s not found in dropdown menu \n" % item) - self.failIf(True) + self.assertTrue(item in unselectable_list, msg=("%s not found in dropdown menu" % item)) self.driver.find_element_by_id("edit-columns-button").click() # step 7 self.driver.find_element_by_partial_link_text("Directory structure").click() head_list = self.get_table_head_text('dirtable') for item in ['Directory / File', 'Symbolic link to', 'Source package', 'Size', 'Permissions', 'Owner', 'Group']: - if item not in head_list: - self.log.error(" %s not found in Directory structure table head \n" % item) - self.failIf(True) - + self.assertTrue(item in head_list, msg=("%s not found in Directory structure table head" % item)) ############## # CASE 950 # @@ -1791,12 +1861,11 @@ class toaster_cases(toaster_cases_base): try: self.find_element_by_link_text_in_table('nav', item) except Exception: - self.log.error("link %s cannot be found in the page" % item) - self.failIf(True) + self.assertFalse(True, msg=("link %s cannot be found in the page" % item)) # step 6 check_list_2 = ['Packages included', 'Total package size', \ 'License manifest', 'Image files'] - self.failUnless(self.is_text_present(check_list_2)) + self.assertTrue(self.is_text_present(check_list_2), msg=("text not in web page")) self.driver.back() try: fail_icon = self.driver.find_element_by_xpath("//*[@class='icon-minus-sign error']") @@ -1813,12 +1882,11 @@ class toaster_cases(toaster_cases_base): try: self.find_element_by_link_text_in_table('nav', item) except Exception: - self.log.error("link %s cannot be found in the page" % item) - self.failIf(True) + self.assertFalse(True, msg=("link %s cannot be found in the page" % item)) # step 7 involved check_list_3 = ['Machine', 'Distro', 'Layers', 'Total number of tasks', 'Tasks executed', \ 'Tasks not executed', 'Reuse', 'Recipes built', 'Packages built'] - self.failUnless(self.is_text_present(check_list_3)) + self.assertTrue(self.is_text_present(check_list_3), msg=("text not in web page")) self.driver.back() @@ -1878,6 +1946,5 @@ class toaster_cases(toaster_cases_base): tasks, recipes, packages need to run manually") self.driver.find_element_by_partial_link_text("Toaster manual").click() if not self.is_text_present("Toaster Manual"): - self.log.error("please check [Toaster manual] link on page") - self.failIf(True) + self.assertFalse(True, msg=("please check [Toaster manual] link on page")) diff --git a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg index 6405f9a8e..685a9ee6a 100644 --- a/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg +++ b/yocto-poky/bitbake/lib/toaster/contrib/tts/toasteruitest/toaster_test.cfg @@ -18,4 +18,8 @@ test_browser = ['ie', 'firefox', 'chrome'] test_cases = [901, 902, 903] logging_level = 'DEBUG' - +[toaster_test_darwin] +toaster_url = 'http://127.0.0.1:8000' +test_browser = 'firefox' +test_cases = [901, 902, 903, 904, 906, 910, 911, 912, 913, 914, 915, 916, 923, 924, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 955, 956] +logging_level = 'INFO' diff --git a/yocto-poky/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py b/yocto-poky/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py new file mode 100644 index 000000000..603060514 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/orm/migrations/0027_auto__add_customimagerecipe__add_unique_customimagerecipe_name_project.py @@ -0,0 +1,375 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CustomImageRecipe' + db.create_table(u'orm_customimagerecipe', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('base_recipe', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Recipe'])), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Project'])), + )) + db.send_create_signal(u'orm', ['CustomImageRecipe']) + + # Adding M2M table for field packages on 'CustomImageRecipe' + m2m_table_name = db.shorten_name(u'orm_customimagerecipe_packages') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('customimagerecipe', models.ForeignKey(orm[u'orm.customimagerecipe'], null=False)), + ('package', models.ForeignKey(orm[u'orm.package'], null=False)) + )) + db.create_unique(m2m_table_name, ['customimagerecipe_id', 'package_id']) + + # Adding unique constraint on 'CustomImageRecipe', fields ['name', 'project'] + db.create_unique(u'orm_customimagerecipe', ['name', 'project_id']) + + + # Changing field 'Package.build' + db.alter_column(u'orm_package', 'build_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['orm.Build'], null=True)) + + def backwards(self, orm): + # Removing unique constraint on 'CustomImageRecipe', fields ['name', 'project'] + db.delete_unique(u'orm_customimagerecipe', ['name', 'project_id']) + + # Deleting model 'CustomImageRecipe' + db.delete_table(u'orm_customimagerecipe') + + # Removing M2M table for field packages on 'CustomImageRecipe' + db.delete_table(db.shorten_name(u'orm_customimagerecipe_packages')) + + + # Changing field 'Package.build' + db.alter_column(u'orm_package', 'build_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['orm.Build'])) + + models = { + u'orm.bitbakeversion': { + 'Meta': {'object_name': 'BitbakeVersion'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + u'orm.branch': { + 'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.build': { + 'Meta': {'object_name': 'Build'}, + 'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'completed_on': ('django.db.models.fields.DateTimeField', [], {}), + 'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'started_on': ('django.db.models.fields.DateTimeField', [], {}) + }, + u'orm.buildartifact': { + 'Meta': {'object_name': 'BuildArtifact'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}), + 'file_size': ('django.db.models.fields.IntegerField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'orm.customimagerecipe': { + 'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'CustomImageRecipe'}, + 'base_recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['orm.Package']", 'symmetrical': 'False'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}) + }, + u'orm.helptext': { + 'Meta': {'object_name': 'HelpText'}, + 'area': ('django.db.models.fields.IntegerField', [], {}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'helptext_build'", 'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + u'orm.layer': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'}, + 'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}) + }, + u'orm.layer_version': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}), + 'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}), + 'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.layersource': { + 'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'}, + 'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}), + 'sourcetype': ('django.db.models.fields.IntegerField', [], {}) + }, + u'orm.layerversiondependency': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'LayerVersionDependency'}, + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependees'", 'to': u"orm['orm.Layer_Version']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': u"orm['orm.Layer_Version']"}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.logmessage': { + 'Meta': {'object_name': 'LogMessage'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'lineno': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}), + 'pathname': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'task': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Task']", 'null': 'True', 'blank': 'True'}) + }, + u'orm.machine': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Machine'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.package': { + 'Meta': {'object_name': 'Package'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']", 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'installed_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']", 'null': 'True'}), + 'revision': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'section': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}) + }, + u'orm.package_dependency': { + 'Meta': {'object_name': 'Package_Dependency'}, + 'dep_type': ('django.db.models.fields.IntegerField', [], {}), + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_target'", 'to': u"orm['orm.Package']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_source'", 'to': u"orm['orm.Package']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']", 'null': 'True'}) + }, + u'orm.package_file': { + 'Meta': {'object_name': 'Package_File'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildfilelist_package'", 'to': u"orm['orm.Package']"}), + 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + u'orm.project': { + 'Meta': {'object_name': 'Project'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) + }, + u'orm.projectlayer': { + 'Meta': {'unique_together': "(('project', 'layercommit'),)", 'object_name': 'ProjectLayer'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layercommit': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}) + }, + u'orm.projecttarget': { + 'Meta': {'object_name': 'ProjectTarget'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + u'orm.projectvariable': { + 'Meta': {'object_name': 'ProjectVariable'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'orm.recipe': { + 'Meta': {'unique_together': "(('layer_version', 'file_path', 'pathflags'),)", 'object_name': 'Recipe'}, + 'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'file_path': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recipe_layer_version'", 'to': u"orm['orm.Layer_Version']"}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'pathflags': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}) + }, + u'orm.recipe_dependency': { + 'Meta': {'object_name': 'Recipe_Dependency'}, + 'dep_type': ('django.db.models.fields.IntegerField', [], {}), + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_depends'", 'to': u"orm['orm.Recipe']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_recipe'", 'to': u"orm['orm.Recipe']"}) + }, + u'orm.release': { + 'Meta': {'object_name': 'Release'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}), + 'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + u'orm.releasedefaultlayer': { + 'Meta': {'object_name': 'ReleaseDefaultLayer'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"}) + }, + u'orm.releaselayersourcepriority': { + 'Meta': {'unique_together': "(('release', 'layer_source'),)", 'object_name': 'ReleaseLayerSourcePriority'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.LayerSource']"}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"}) + }, + u'orm.target': { + 'Meta': {'object_name': 'Target'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license_manifest_path': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + u'orm.target_file': { + 'Meta': {'object_name': 'Target_File'}, + 'directory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'directory_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}), + 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inodetype': ('django.db.models.fields.IntegerField', [], {}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}), + 'permission': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'sym_target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'symlink_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.target_image_file': { + 'Meta': {'object_name': 'Target_Image_File'}, + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '254'}), + 'file_size': ('django.db.models.fields.IntegerField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.target_installed_package': { + 'Meta': {'object_name': 'Target_Installed_Package'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildtargetlist_package'", 'to': u"orm['orm.Package']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.task': { + 'Meta': {'ordering': "('order', 'recipe')", 'unique_together': "(('build', 'recipe', 'task_name'),)", 'object_name': 'Task'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_build'", 'to': u"orm['orm.Build']"}), + 'cpu_usage': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}), + 'disk_io': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'elapsed_time': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'logfile': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}), + 'order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'path_to_sstate_obj': ('django.db.models.fields.FilePathField', [], {'max_length': '500', 'blank': 'True'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'to': u"orm['orm.Recipe']"}), + 'script_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'source_url': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'sstate_checksum': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'sstate_result': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'task_executed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'work_directory': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}) + }, + u'orm.task_dependency': { + 'Meta': {'object_name': 'Task_Dependency'}, + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_depends'", 'to': u"orm['orm.Task']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'task': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_task'", 'to': u"orm['orm.Task']"}) + }, + u'orm.toastersetting': { + 'Meta': {'object_name': 'ToasterSetting'}, + 'helptext': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '63'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'orm.variable': { + 'Meta': {'object_name': 'Variable'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_build'", 'to': u"orm['orm.Build']"}), + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'human_readable_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'variable_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'variable_value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'orm.variablehistory': { + 'Meta': {'object_name': 'VariableHistory'}, + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_number': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'variable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vhistory'", 'to': u"orm['orm.Variable']"}) + } + } + + complete_apps = ['orm']
\ No newline at end of file diff --git a/yocto-poky/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py b/yocto-poky/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py new file mode 100644 index 000000000..a2f8661af --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/orm/migrations/0028_auto__chg_field_logmessage_message.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'LogMessage.message' + db.alter_column(u'orm_logmessage', 'message', self.gf('django.db.models.fields.TextField')(null=True)) + + def backwards(self, orm): + + # Changing field 'LogMessage.message' + db.alter_column(u'orm_logmessage', 'message', self.gf('django.db.models.fields.CharField')(default='', max_length=240)) + + models = { + u'orm.bitbakeversion': { + 'Meta': {'object_name': 'BitbakeVersion'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'giturl': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + u'orm.branch': { + 'Meta': {'unique_together': "(('layer_source', 'name'), ('layer_source', 'up_id'))", 'object_name': 'Branch'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'True', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.build': { + 'Meta': {'object_name': 'Build'}, + 'bitbake_version': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'build_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'completed_on': ('django.db.models.fields.DateTimeField', [], {}), + 'cooker_log_path': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'distro': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'distro_version': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'machine': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '2'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'started_on': ('django.db.models.fields.DateTimeField', [], {}) + }, + u'orm.buildartifact': { + 'Meta': {'object_name': 'BuildArtifact'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}), + 'file_size': ('django.db.models.fields.IntegerField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'orm.customimagerecipe': { + 'Meta': {'unique_together': "(('name', 'project'),)", 'object_name': 'CustomImageRecipe'}, + 'base_recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['orm.Package']", 'symmetrical': 'False'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}) + }, + u'orm.helptext': { + 'Meta': {'object_name': 'HelpText'}, + 'area': ('django.db.models.fields.IntegerField', [], {}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'helptext_build'", 'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + u'orm.layer': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'), ('layer_source', 'name'))", 'object_name': 'Layer'}, + 'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_index_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'summary': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'vcs_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_file_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_tree_base_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}), + 'vcs_web_url': ('django.db.models.fields.URLField', [], {'default': 'None', 'max_length': '200', 'null': 'True'}) + }, + u'orm.layer_version': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Layer_Version'}, + 'branch': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'layer_version_build'", 'null': 'True', 'to': u"orm['orm.Build']"}), + 'commit': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dirpath': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'layer_version_layer'", 'to': u"orm['orm.Layer']"}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'local_path': ('django.db.models.fields.FilePathField', [], {'default': "'/'", 'max_length': '1024'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Project']", 'null': 'True'}), + 'up_branch': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.Branch']", 'null': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.layersource': { + 'Meta': {'unique_together': "(('sourcetype', 'apiurl'),)", 'object_name': 'LayerSource'}, + 'apiurl': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '63'}), + 'sourcetype': ('django.db.models.fields.IntegerField', [], {}) + }, + u'orm.layerversiondependency': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'LayerVersionDependency'}, + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependees'", 'to': u"orm['orm.Layer_Version']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': u"orm['orm.Layer_Version']"}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.logmessage': { + 'Meta': {'object_name': 'LogMessage'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'lineno': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'pathname': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'task': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Task']", 'null': 'True', 'blank': 'True'}) + }, + u'orm.machine': { + 'Meta': {'unique_together': "(('layer_source', 'up_id'),)", 'object_name': 'Machine'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}) + }, + u'orm.package': { + 'Meta': {'object_name': 'Package'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']", 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'installed_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Recipe']", 'null': 'True'}), + 'revision': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'section': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}) + }, + u'orm.package_dependency': { + 'Meta': {'object_name': 'Package_Dependency'}, + 'dep_type': ('django.db.models.fields.IntegerField', [], {}), + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_target'", 'to': u"orm['orm.Package']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_dependencies_source'", 'to': u"orm['orm.Package']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']", 'null': 'True'}) + }, + u'orm.package_file': { + 'Meta': {'object_name': 'Package_File'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildfilelist_package'", 'to': u"orm['orm.Package']"}), + 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + u'orm.project': { + 'Meta': {'object_name': 'Project'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']", 'null': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']", 'null': 'True'}), + 'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) + }, + u'orm.projectlayer': { + 'Meta': {'unique_together': "(('project', 'layercommit'),)", 'object_name': 'ProjectLayer'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layercommit': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Layer_Version']", 'null': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}) + }, + u'orm.projecttarget': { + 'Meta': {'object_name': 'ProjectTarget'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + u'orm.projectvariable': { + 'Meta': {'object_name': 'ProjectVariable'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Project']"}), + 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'orm.recipe': { + 'Meta': {'unique_together': "(('layer_version', 'file_path', 'pathflags'),)", 'object_name': 'Recipe'}, + 'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'file_path': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}), + 'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['orm.LayerSource']", 'null': 'True'}), + 'layer_version': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'recipe_layer_version'", 'to': u"orm['orm.Layer_Version']"}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'pathflags': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), + 'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'up_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'up_id': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}) + }, + u'orm.recipe_dependency': { + 'Meta': {'object_name': 'Recipe_Dependency'}, + 'dep_type': ('django.db.models.fields.IntegerField', [], {}), + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_depends'", 'to': u"orm['orm.Recipe']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'r_dependencies_recipe'", 'to': u"orm['orm.Recipe']"}) + }, + u'orm.release': { + 'Meta': {'object_name': 'Release'}, + 'bitbake_version': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.BitbakeVersion']"}), + 'branch_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'helptext': ('django.db.models.fields.TextField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}) + }, + u'orm.releasedefaultlayer': { + 'Meta': {'object_name': 'ReleaseDefaultLayer'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"}) + }, + u'orm.releaselayersourcepriority': { + 'Meta': {'unique_together': "(('release', 'layer_source'),)", 'object_name': 'ReleaseLayerSourcePriority'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.LayerSource']"}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Release']"}) + }, + u'orm.target': { + 'Meta': {'object_name': 'Target'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Build']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image_size': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'is_image': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license_manifest_path': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'task': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + u'orm.target_file': { + 'Meta': {'object_name': 'Target_File'}, + 'directory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'directory_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}), + 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inodetype': ('django.db.models.fields.IntegerField', [], {}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'path': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}), + 'permission': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'sym_target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'symlink_set'", 'null': 'True', 'to': u"orm['orm.Target_File']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.target_image_file': { + 'Meta': {'object_name': 'Target_Image_File'}, + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '254'}), + 'file_size': ('django.db.models.fields.IntegerField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.target_installed_package': { + 'Meta': {'object_name': 'Target_Installed_Package'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buildtargetlist_package'", 'to': u"orm['orm.Package']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['orm.Target']"}) + }, + u'orm.task': { + 'Meta': {'ordering': "('order', 'recipe')", 'unique_together': "(('build', 'recipe', 'task_name'),)", 'object_name': 'Task'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_build'", 'to': u"orm['orm.Build']"}), + 'cpu_usage': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}), + 'disk_io': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'elapsed_time': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '8', 'decimal_places': '2'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'logfile': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '240'}), + 'order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'outcome': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'path_to_sstate_obj': ('django.db.models.fields.FilePathField', [], {'max_length': '500', 'blank': 'True'}), + 'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'to': u"orm['orm.Recipe']"}), + 'script_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'source_url': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}), + 'sstate_checksum': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'sstate_result': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'task_executed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'task_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'work_directory': ('django.db.models.fields.FilePathField', [], {'max_length': '255', 'blank': 'True'}) + }, + u'orm.task_dependency': { + 'Meta': {'object_name': 'Task_Dependency'}, + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_depends'", 'to': u"orm['orm.Task']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'task': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_dependencies_task'", 'to': u"orm['orm.Task']"}) + }, + u'orm.toastersetting': { + 'Meta': {'object_name': 'ToasterSetting'}, + 'helptext': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '63'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'orm.variable': { + 'Meta': {'object_name': 'Variable'}, + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variable_build'", 'to': u"orm['orm.Build']"}), + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'human_readable_name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'variable_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'variable_value': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + u'orm.variablehistory': { + 'Meta': {'object_name': 'VariableHistory'}, + 'file_name': ('django.db.models.fields.FilePathField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_number': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'value': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'variable': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vhistory'", 'to': u"orm['orm.Variable']"}) + } + } + + complete_apps = ['orm']
\ No newline at end of file diff --git a/yocto-poky/bitbake/lib/toaster/orm/models.py b/yocto-poky/bitbake/lib/toaster/orm/models.py index e4d2e87ae..383290583 100644 --- a/yocto-poky/bitbake/lib/toaster/orm/models.py +++ b/yocto-poky/bitbake/lib/toaster/orm/models.py @@ -191,10 +191,11 @@ class Project(models.Model): # returns a queryset of compatible layers for a project def compatible_layerversions(self, release = None, layer_name = None): + logger.warning("This function is deprecated") if release == None: release = self.release # layers on the same branch or layers specifically set for this project - queryset = Layer_Version.objects.filter((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self) | Q(build__project = self)) + queryset = Layer_Version.objects.filter(((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self)) & Q(build__isnull=True)) if layer_name is not None: # we select only a layer name @@ -205,45 +206,55 @@ class Project(models.Model): return queryset - def projectlayer_equivalent_set(self): - return self.compatible_layerversions().filter(layer__name__in = [x.layercommit.layer.name for x in self.projectlayer_set.all()]).select_related("up_branch") + def get_all_compatible_layer_versions(self): + """ Returns Queryset of all Layer_Versions which are compatible with + this project""" + queryset = Layer_Version.objects.filter( + (Q(up_branch__name=self.release.branch_name) & Q(build=None)) + | Q(project=self)) + + return queryset + + def get_project_layer_versions(self, pk=False): + """ Returns the Layer_Versions currently added to this project """ + layer_versions = self.projectlayer_set.all().values('layercommit') + + if pk is False: + return layer_versions + else: + return layer_versions.values_list('layercommit__pk', flat=True) + def get_available_machines(self): """ Returns QuerySet of all Machines which are provided by the Layers currently added to the Project """ - queryset = Machine.objects.filter(layer_version__in=self.projectlayer_equivalent_set) + queryset = Machine.objects.filter( + layer_version__in=self.get_project_layer_versions()) + return queryset def get_all_compatible_machines(self): """ Returns QuerySet of all the compatible machines available to the project including ones from Layers not currently added """ - compatible_layers = self.compatible_layerversions() + queryset = Machine.objects.filter( + layer_version__in=self.get_all_compatible_layer_versions()) - queryset = Machine.objects.filter(layer_version__in=compatible_layers) return queryset def get_available_recipes(self): - """ Returns QuerySet of all Recipes which are provided by the Layers - currently added to the Project """ - project_layers = self.projectlayer_equivalent_set() - queryset = Recipe.objects.filter(layer_version__in = project_layers) - - # Copied from get_all_compatible_recipes - search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id'))) - queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source') - # End copy + """ Returns QuerySet of all the recipes that are provided by layers + added to this project """ + queryset = Recipe.objects.filter( + layer_version__in=self.get_project_layer_versions()) return queryset def get_all_compatible_recipes(self): """ Returns QuerySet of all the compatible Recipes available to the project including ones from Layers not currently added """ - compatible_layerversions = self.compatible_layerversions() - queryset = Recipe.objects.filter(layer_version__in = compatible_layerversions) + queryset = Recipe.objects.filter( + layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='') - search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id'))) - - queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source') return queryset @@ -260,7 +271,7 @@ class Project(models.Model): for l in self.projectlayer_set.all().order_by("pk"): commit = l.layercommit.get_vcs_reference() print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit) - BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath) + BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath, layer_version=l.layercommit) br.state = BuildRequest.REQ_QUEUED now = timezone.now() @@ -270,7 +281,7 @@ class Project(models.Model): ) for t in self.projecttarget_set.all(): BRTarget.objects.create(req = br, target = t.target, task = t.task) - Target.objects.create(build = br.build, target = t.target) + Target.objects.create(build = br.build, target = t.target, task = t.task) for v in self.projectvariable_set.all(): BRVariable.objects.create(req = br, name = v.name, value = v.value) @@ -333,13 +344,14 @@ class Build(models.Model): tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); return( tgts ); - @property - def toaster_exceptions(self): - return self.logmessage_set.filter(level=LogMessage.EXCEPTION) + def get_outcome_text(self): + return Build.BUILD_OUTCOME[int(self.outcome)][1] @property def errors(self): - return (self.logmessage_set.filter(level=LogMessage.ERROR)|self.logmessage_set.filter(level=LogMessage.EXCEPTION)) + return (self.logmessage_set.filter(level=LogMessage.ERROR) | + self.logmessage_set.filter(level=LogMessage.EXCEPTION) | + self.logmessage_set.filter(level=LogMessage.CRITICAL)) @property def warnings(self): @@ -350,10 +362,23 @@ class Build(models.Model): return (self.completed_on - self.started_on).total_seconds() def get_current_status(self): + """ + get the status string from the build request if the build + has one, or the text for the build outcome if it doesn't + """ + from bldcontrol.models import BuildRequest - if self.outcome == Build.IN_PROGRESS and self.buildrequest.state != BuildRequest.REQ_INPROGRESS: + + build_request = None + if hasattr(self, 'buildrequest'): + build_request = self.buildrequest + + if (build_request + and build_request.state != BuildRequest.REQ_INPROGRESS + and self.outcome == Build.IN_PROGRESS): return self.buildrequest.get_state_display() - return self.get_outcome_display() + else: + return self.get_outcome_text() def __str__(self): return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()])) @@ -551,7 +576,7 @@ class Task_Dependency(models.Model): class Package(models.Model): search_allowed_fields = ['name', 'version', 'revision', 'recipe__name', 'recipe__version', 'recipe__license', 'recipe__layer_version__layer__name', 'recipe__layer_version__branch', 'recipe__layer_version__commit', 'recipe__layer_version__local_path', 'installed_name'] - build = models.ForeignKey('Build') + build = models.ForeignKey('Build', null=True) recipe = models.ForeignKey('Recipe', null=True) name = models.CharField(max_length=100) installed_name = models.CharField(max_length=100, default='') @@ -828,6 +853,7 @@ class LayerIndexLayerSource(LayerSource): import urllib2, urlparse, json import os proxy_settings = os.environ.get("http_proxy", None) + oe_core_layer = 'openembedded-core' def _get_json_response(apiurl = self.apiurl): _parsedurl = urlparse.urlparse(apiurl) @@ -872,6 +898,25 @@ class LayerIndexLayerSource(LayerSource): if not connection.features.autocommits_when_autocommit_is_off: transaction.set_autocommit(False) for li in layers_info: + # Special case for the openembedded-core layer + if li['name'] == oe_core_layer: + try: + # If we have an existing openembedded-core for example + # from the toasterconf.json augment the info using the + # layerindex rather than duplicate it + oe_core_l = Layer.objects.get(name=oe_core_layer) + # Take ownership of the layer as now coming from the + # layerindex + oe_core_l.layer_source = self + oe_core_l.up_id = li['id'] + oe_core_l.summary = li['summary'] + oe_core_l.description = li['description'] + oe_core_l.save() + continue + + except Layer.DoesNotExist: + pass + l, created = Layer.objects.get_or_create(layer_source = self, name = li['name']) l.up_id = li['id'] l.up_date = li['updated'] @@ -882,6 +927,7 @@ class LayerIndexLayerSource(LayerSource): l.summary = li['summary'] l.description = li['description'] l.save() + if not connection.features.autocommits_when_autocommit_is_off: transaction.set_autocommit(True) @@ -974,9 +1020,12 @@ class LayerIndexLayerSource(LayerSource): ro.file_path = ri['filepath'] + "/" + ri['filename'] if 'inherits' in ri: ro.is_image = 'image' in ri['inherits'].split() + else: # workaround for old style layer index + ro.is_image = "-image-" in ri['pn'] ro.save() except IntegrityError as e: logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename'])) + ro.delete() if not connection.features.autocommits_when_autocommit_is_off: transaction.set_autocommit(True) @@ -1132,17 +1181,36 @@ class Layer_Version(models.Model): return project.compatible_layerversions(layer_name = self.layer.name) def get_vcs_reference(self): - if self.commit is not None and len(self.commit) > 0: - return self.commit if self.branch is not None and len(self.branch) > 0: return self.branch if self.up_branch is not None: return self.up_branch.name + if self.commit is not None and len(self.commit) > 0: + return self.commit return ("Cannot determine the vcs_reference for layer version %s" % vars(self)) def get_detailspage_url(self, project_id): return reverse('layerdetails', args=(project_id, self.pk)) + def get_alldeps(self, project_id): + """Get full list of unique layer dependencies.""" + def gen_layerdeps(lver, project): + for ldep in lver.dependencies.all(): + yield ldep.depends_on + # get next level of deps recursively calling gen_layerdeps + for subdep in gen_layerdeps(ldep.depends_on, project): + yield subdep + + project = Project.objects.get(pk=project_id) + result = [] + projectlvers = [player.layercommit for player in project.projectlayer_set.all()] + for dep in gen_layerdeps(self, project): + # filter out duplicates and layers already belonging to the project + if dep not in result + projectlvers: + result.append(dep) + + return sorted(result, key=lambda x: x.layer.name) + def __unicode__(self): return "%d %s (VCS %s, Project %s)" % (self.pk, str(self.layer), self.get_vcs_reference(), self.build.project if self.build is not None else "No project") @@ -1170,6 +1238,15 @@ class ProjectLayer(models.Model): class Meta: unique_together = (("project", "layercommit"),) +class CustomImageRecipe(models.Model): + name = models.CharField(max_length=100) + base_recipe = models.ForeignKey(Recipe) + packages = models.ManyToManyField(Package) + project = models.ForeignKey(Project) + + class Meta: + unique_together = ("name", "project") + class ProjectVariable(models.Model): project = models.ForeignKey(Project) name = models.CharField(max_length=100) @@ -1206,16 +1283,20 @@ class LogMessage(models.Model): INFO = 0 WARNING = 1 ERROR = 2 - - LOG_LEVEL = ( (INFO, "info"), - (WARNING, "warn"), - (ERROR, "error"), - (EXCEPTION, "toaster exception")) + CRITICAL = 3 + + LOG_LEVEL = ( + (INFO, "info"), + (WARNING, "warn"), + (ERROR, "error"), + (CRITICAL, "critical"), + (EXCEPTION, "toaster exception") + ) build = models.ForeignKey(Build) task = models.ForeignKey(Task, blank = True, null=True) level = models.IntegerField(choices=LOG_LEVEL, default=INFO) - message=models.CharField(max_length=240) + message = models.TextField(blank=True, null=True) pathname = models.FilePathField(max_length=255, blank=True) lineno = models.IntegerField(null=True) diff --git a/yocto-poky/bitbake/lib/toaster/orm/tests.py b/yocto-poky/bitbake/lib/toaster/orm/tests.py index 783aea890..719266e6d 100644 --- a/yocto-poky/bitbake/lib/toaster/orm/tests.py +++ b/yocto-poky/bitbake/lib/toaster/orm/tests.py @@ -23,12 +23,11 @@ from django.test import TestCase, TransactionTestCase from orm.models import LocalLayerSource, LayerIndexLayerSource, ImportedLayerSource, LayerSource -from orm.models import Branch +from orm.models import Branch, LayerVersionDependency -from orm.models import Project, Build, Layer, Layer_Version, Branch, ProjectLayer +from orm.models import Project, Layer, Layer_Version, Branch, ProjectLayer from orm.models import Release, ReleaseLayerSourcePriority, BitbakeVersion -from django.utils import timezone from django.db import IntegrityError import os @@ -153,35 +152,29 @@ class LayerVersionEquivalenceTestCase(TestCase): equivqs = self.lver.get_equivalents_wpriority(self.project) self.assertEqual(list(equivqs), [lver2, self.lver]) - def test_build_layerversion(self): + def test_compatible_layer_versions(self): """ - Any layer version coming from the build should show up - before any layer version coming from upstream - """ - build = Build.objects.create(project=self.project, - started_on=timezone.now(), - completed_on=timezone.now()) - lvb = Layer_Version.objects.create(layer=self.layer, build=build, - commit="deadbeef") - - # a build layerversion must be in the equivalence - # list for the original layerversion - equivqs = self.lver.get_equivalents_wpriority(self.project) - self.assertTrue(len(equivqs) == 2) - self.assertTrue(equivqs[0] == self.lver) - self.assertTrue(equivqs[1] == lvb) - - # getting the build layerversion equivalent list must - # return the same list as the original layer - bequivqs = lvb.get_equivalents_wpriority(self.project) - - self.assertEqual(list(equivqs), list(bequivqs)) - - def test_compatible_layerversions(self): - """ - When we have a 2 layer versions, compatible_layerversions() + When we have a 2 layer versions, get_all_compatible_layerversions() should return a queryset with both. """ - compat_lv = self.project.compatible_layerversions() + compat_lv = self.project.get_all_compatible_layer_versions() self.assertEqual(list(compat_lv), [self.lver, self.lver2]) + def test_layerversion_get_alldeps(self): + """Test Layer_Version.get_alldeps API.""" + lvers = {} + for i in range(10): + name = "layer%d" % i + lvers[name] = Layer_Version.objects.create(layer=Layer.objects.create(name=name), + project=self.project) + if i: + LayerVersionDependency.objects.create(layer_version=lvers["layer%d" % (i - 1)], + depends_on=lvers[name]) + # Check dinamically added deps + self.assertEqual(lvers['layer0'].get_alldeps(self.project.id), + [lvers['layer%d' % n] for n in range(1, i+1)]) + + # Check chain of deps created in previous loop + for i in range(10): + self.assertEqual(lvers['layer%d' % i].get_alldeps(self.project.id), + [lvers['layer%d' % n] for n in range(i+1, 10)]) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css index cce3e315f..bc8a97bed 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/css/default.css @@ -15,6 +15,8 @@ /* Styles for the help information */ .get-help { color: #CCCCCC; } .get-help:hover, .icon-plus-sign:hover { color: #999999; cursor: pointer; } +.get-help-green { color: #468847; } +.get-help-green:hover { color: #347132; cursor: pointer; } .get-help-blue { color: #3A87AD; } .get-help-blue:hover { color: #005580; cursor: pointer; } .get-help-yellow { color: #C09853; } @@ -161,9 +163,16 @@ table { table-layout: fixed; word-wrap: break-word; } .project-name .label { font-weight: normal; margin-bottom: 5px; margin-left: -15px; padding: 5px; } .project-name .label > a { color: #fff; font-weight: normal; } +/* styles for showing help icons next to command-line builds */ +.build-result .get-help-green, .build-result .get-help-red, .build-result .get-help-blue { margin-right: 35px; margin-top: 8px; font-size: 16px; } + /* Remove bottom margin for forms inside modal dialogs */ #dependencies-modal-form { margin-bottom: 0px; } +/* Custom column widths */ +.narrow-col { width: 8%; } +.medium-col { width: 12%; } + /* Configuration styles */ .icon-trash { color: #B94A48; font-size: 16px; padding-left: 5px; } .icon-trash:hover { color: #943A38; text-decoration: none; cursor: pointer; } diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js index 895e61b2a..ed22a4ebc 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/base.js @@ -6,6 +6,7 @@ function basePageInit(ctx) { var newBuildTargetInput; var newBuildTargetBuildBtn; var projectNameForm = $("#project-name-change-form"); + var projectNameContainer = $("#project-name-container"); var projectName = $("#project-name"); var projectNameFormToggle = $("#project-change-form-toggle"); var projectNameChangeCancel = $("#project-name-change-cancel"); @@ -23,24 +24,21 @@ function basePageInit(ctx) { /* Project name change functionality */ projectNameFormToggle.click(function(e){ e.preventDefault(); - - $(this).add(projectName).hide(); + projectNameContainer.hide(); projectNameForm.fadeIn(); }); projectNameChangeCancel.click(function(e){ e.preventDefault(); - projectNameForm.hide(); - projectName.add(projectNameFormToggle).fadeIn(); + projectNameContainer.fadeIn(); }); $("#project-name-change-btn").click(function(e){ var newProjectName = $("#project-name-change-input").val(); - libtoaster.editCurrentProject({ projectName: newProjectName },function (){ - - projectName.text(newProjectName); + libtoaster.editCurrentProject({ projectName: newProjectName }, function (){ + projectName.html(newProjectName); libtoaster.ctx.projectName = newProjectName; projectNameChangeCancel.click(); }); @@ -123,14 +121,14 @@ function basePageInit(ctx) { }); function _checkProjectBuildable() { - if (selectedProject.projectId === undefined) { + if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) { return; } libtoaster.getProjectInfo(selectedProject.projectPageUrl, function (data) { if (data.machine === null || data.machine.name === undefined || data.layers.length === 0) { - /* we can't build anything with out a machine and some layers */ + /* we can't build anything without a machine and some layers */ $("#new-build-button #targets-form").hide(); $("#new-build-button .alert").show(); } else { @@ -149,7 +147,7 @@ function basePageInit(ctx) { /* If we don't have a current project then present the set project * form. */ - if (selectedProject.projectId === undefined) { + if (selectedProject.projectId === undefined || selectedProject.projectIsDefault) { $('#change-project-form').show(); $('#project .icon-pencil').hide(); } diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js new file mode 100644 index 000000000..4f6b304dd --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/customrecipe.js @@ -0,0 +1,50 @@ +"use strict"; + +function customRecipePageInit(ctx) { + + var urlParams = libtoaster.parseUrlParams(); + + (function notificationRequest(){ + if (urlParams.hasOwnProperty('notify') && urlParams.notify === 'new'){ + $("#image-created-notification").show(); + } + })(); + + $("#recipeselection").on('table-done', function(e, total, tableParams){ + /* Table is done so now setup the click handler for the package buttons */ + $(".add-rm-package-btn").click(function(e){ + e.preventDefault(); + addRemovePackage($(this), tableParams); + }); + }); + + function addRemovePackage(pkgBtn, tableParams){ + var pkgBtnData = pkgBtn.data(); + var method; + var buttonToShow; + + if (pkgBtnData.directive == 'add') { + method = 'PUT'; + buttonToShow = '#package-rm-btn-' + pkgBtnData.package; + } else if (pkgBtnData.directive == 'remove') { + method = 'DELETE'; + buttonToShow = '#package-add-btn-' + pkgBtnData.package; + } else { + throw("Unknown package directive: should be add or remove"); + } + + $.ajax({ + type: method, + url: pkgBtnData.packageUrl, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function(data){ + /* Invalidate the Add | Rm package table's current cache */ + tableParams.nocache = true; + $.get(ctx.tableApiUrl, tableParams); + /* Swap the buttons around */ + pkgBtn.hide(); + $(buttonToShow).show(); + } + }); + } +} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js index 2fadbc097..c68f3669f 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/importlayer.js @@ -195,8 +195,8 @@ function importLayerPageInit (ctx) { var dupLayerInfo = $("#duplicate-layer-info"); dupLayerInfo.find(".dup-layer-name").text(layer.name); dupLayerInfo.find(".dup-layer-link").attr("href", layer.layerdetailurl); - dupLayerInfo.find("#dup-layer-vcs-url").text(layer.layer__vcs_url); - dupLayerInfo.find("#dup-layer-revision").text(layer.revision.commit); + dupLayerInfo.find("#dup-layer-vcs-url").text(layer.vcs_url); + dupLayerInfo.find("#dup-layer-revision").text(layer.vcs_reference); $(".fields-apart-from-layer-name").fadeOut(function(){ @@ -214,11 +214,10 @@ function importLayerPageInit (ctx) { $.getJSON(libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" , search: name, format: "json" }, function(layer) { - if (layer.rows.length > 0) { - for (var i in layer.rows){ - if (layer.rows[i].name == name) { - console.log(layer.rows[i]) - layerExistsError(layer.rows[i]); + if (layer.results.length > 0) { + for (var i in layer.results){ + if (layer.results[i].name == name) { + layerExistsError(layer.results[i]); } } } diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js index 42e7427cc..794b902c5 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/jquery.treetable.js @@ -421,7 +421,7 @@ columnElType: "td", // i.e. 'td', 'th' or 'td,th' expandable: false, expanderTemplate: "<a href='#'> </a>", - indent: 19, + indent: 10, indenterTemplate: "<span class='indenter'></span>", initialState: "collapsed", nodeIdAttr: "ttId", // maps to data-tt-id diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js index a0509f9aa..7318b3f50 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerBtn.js @@ -1,6 +1,6 @@ "use strict"; -function layerBtnsInit(ctx) { +function layerBtnsInit() { /* Remove any current bindings to avoid duplicated binds */ $(".layerbtn").unbind('click'); @@ -68,10 +68,16 @@ function layerBtnsInit(ctx) { }); }); - /* Setup the initial state of the buttons */ - for (var i in ctx.projectLayers){ - $(".layer-exists-" + ctx.projectLayers[i]).show(); - $(".layer-add-" + ctx.projectLayers[i]).hide(); - } + $(".customise-btn").unbind('click'); + $(".customise-btn").click(function(e){ + e.preventDefault(); + var imgCustomModal = $("#new-custom-image-modal"); + + if (imgCustomModal.length == 0) + throw("Modal new-custom-image not found"); + + imgCustomModal.data('recipe', $(this).data('recipe')); + imgCustomModal.modal('show'); + }); } diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js index 000e8038f..8c2ec4cdd 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/layerdetails.js @@ -7,6 +7,9 @@ function layerDetailsPageInit (ctx) { var layerDepsList = $("#layer-deps-list"); var currentLayerDepSelection; var addRmLayerBtn = $("#add-remove-layer-btn"); + var targetTab = $("#targets-tab"); + var machineTab = $("#machines-tab"); + var detailsTab = $("#details-tab"); /* setup the dependencies typeahead */ libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.layersTypeAheadUrl, { include_added: "true" }, function(item){ @@ -15,6 +18,21 @@ function layerDetailsPageInit (ctx) { layerDepBtn.removeAttr("disabled"); }); + $(window).on('hashchange', function(e){ + switch(window.location.hash){ + case '#machines': + machineTab.tab('show'); + break; + case '#recipes': + targetTab.tab('show'); + break; + default: + detailsTab.tab('show'); + break; + } + }); + + $(".breadcrumb li:first a").click(function(e){ e.preventDefault(); /* By default this link goes to the project configuration page. However @@ -143,7 +161,7 @@ function layerDetailsPageInit (ctx) { addRmLayerBtn.removeClass("btn-danger"); } - $("#details-tab").on('show', function(){ + detailsTab.on('show', function(){ if (!ctx.layerVersion.inCurrentPrj) defaultAddBtnText(); @@ -174,7 +192,7 @@ function layerDetailsPageInit (ctx) { $("#no-recipes-yet").hide(); } - $("#targets-tab").removeClass("muted"); + targetTab.removeClass("muted"); if (window.location.hash === "#recipes"){ /* re run the machinesTabShow to update the text */ targetsTabShow(); @@ -189,7 +207,7 @@ function layerDetailsPageInit (ctx) { else $("#no-machines-yet").hide(); - $("#machines-tab").removeClass("muted"); + machineTab.removeClass("muted"); if (window.location.hash === "#machines"){ /* re run the machinesTabShow to update the text */ machinesTabShow(); @@ -202,7 +220,7 @@ function layerDetailsPageInit (ctx) { }); - $("#targets-tab").on('show', targetsTabShow); + targetTab.on('show', targetsTabShow); function machinesTabShow(){ if (!ctx.layerVersion.inCurrentPrj) { @@ -219,7 +237,7 @@ function layerDetailsPageInit (ctx) { window.location.hash = "machines"; } - $("#machines-tab").on('show', machinesTabShow); + machineTab.on('show', machinesTabShow); $(".pagesize").change(function(){ var search = libtoaster.parseUrlParams(); @@ -236,7 +254,7 @@ function layerDetailsPageInit (ctx) { if (added){ /* enable and switch all the button states */ - $(".build-target-btn").removeAttr("disabled"); + $(".build-recipe-btn").removeAttr("disabled"); $(".select-machine-btn").removeAttr("disabled"); addRmLayerBtn.addClass("btn-danger"); addRmLayerBtn.data('directive', "remove"); @@ -245,7 +263,7 @@ function layerDetailsPageInit (ctx) { } else { /* disable and switch all the button states */ - $(".build-target-btn").attr("disabled","disabled"); + $(".build-recipe-btn").attr("disabled","disabled"); $(".select-machine-btn").attr("disabled", "disabled"); addRmLayerBtn.removeClass("btn-danger"); addRmLayerBtn.data('directive', "add"); diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js new file mode 100644 index 000000000..935b21ede --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/newcustomimage.js @@ -0,0 +1,49 @@ +"use strict"; + +function newCustomImagePageInit(ctx){ + + var newCustomImgBtn = $("#create-new-custom-image-btn"); + var imgCustomModal = $("#new-custom-image-modal"); + + newCustomImgBtn.click(function(e){ + e.preventDefault(); + + var name = imgCustomModal.find('input').val(); + var baseRecipeId = imgCustomModal.data('recipe'); + + if (name.length > 0) { + createCustomRecipe(name, baseRecipeId); + imgCustomModal.modal('hide'); + } else { + console.warn("TODO No name supplied"); + } + }); + + function createCustomRecipe(name, baseRecipeId){ + var data = { + 'name' : name, + 'project' : libtoaster.ctx.projectId, + 'base' : baseRecipeId, + }; + + $.ajax({ + type: "POST", + url: ctx.xhrCustomRecipeUrl, + data: data, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (ret) { + if (ret.error !== "ok") { + console.warn(ret.error); + } else { + window.location.replace(ret.url + '?notify=new'); + } + }, + error: function (ret) { + console.warn("Call failed"); + console.warn(ret); + } + }); + } + + +} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js index d36704751..e742ef291 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/projectpage.js @@ -23,7 +23,7 @@ function projectPageInit(ctx) { var cancelReleaseChange = $("#cancel-release-change"); var currentLayerAddSelection; - var currentMachineAddSelection = {}; + var currentMachineAddSelection = ""; var urlParams = libtoaster.parseUrlParams(); @@ -38,7 +38,7 @@ function projectPageInit(ctx) { */ if (urlParams.hasOwnProperty('setMachine') && urlParams.setMachine !== prjInfo.machine.name){ - currentMachineAddSelection.name = urlParams.setMachine; + machineChangeInput.val(urlParams.setMachine); machineChangeBtn.click(); } else { updateMachineName(prjInfo.machine.name); @@ -103,6 +103,12 @@ function projectPageInit(ctx) { layerAddBtn.removeAttr("disabled"); }); + layerAddInput.keyup(function() { + if ($(this).val().length == 0) { + layerAddBtn.attr("disabled", "disabled") + } + }); + layerAddBtn.click(function(e){ e.preventDefault(); var layerObj = currentLayerAddSelection; @@ -146,10 +152,7 @@ function projectPageInit(ctx) { link.attr("href", layerObj.layerdetailurl); link.text(layerObj.name); - /* YOCTO #8024 - link.tooltip({title: layerObj.giturl + " | "+ layerObj.branch.name, placement: "right"}); - branch name not accessible sometimes it is revision instead - */ + link.tooltip({title: layerObj.vcs_url + " | "+ layerObj.vcs_reference, placement: "right"}); var trashItem = projectLayer.children("span"); trashItem.click(function (e) { @@ -251,29 +254,33 @@ function projectPageInit(ctx) { } libtoaster.makeTypeahead(machineChangeInput, libtoaster.ctx.machinesTypeAheadUrl, { }, function(item){ - currentMachineAddSelection = item; + currentMachineAddSelection = item.name; machineChangeBtn.removeAttr("disabled"); }); machineChangeBtn.click(function(e){ e.preventDefault(); - if (currentMachineAddSelection.name === undefined) + /* We accept any value regardless of typeahead selection or not */ + if (machineChangeInput.val().length === 0) return; - libtoaster.editCurrentProject({ machineName : currentMachineAddSelection.name }, + currentMachineAddSelection = machineChangeInput.val(); + + libtoaster.editCurrentProject( + { machineName : currentMachineAddSelection }, function(){ /* Success machine changed */ - updateMachineName(currentMachineAddSelection.name); + updateMachineName(currentMachineAddSelection); machineChangeCancel.click(); /* Show the alert message */ var message = $('<span class="lead">You have changed the machine to: <strong><span id="notify-machine-name"></span></strong></span>'); - message.find("#notify-machine-name").text(currentMachineAddSelection.name); + message.find("#notify-machine-name").text(currentMachineAddSelection); libtoaster.showChangeNotification(message); }, function(){ /* Failed machine changed */ - console.log("failed to change machine"); + console.warn("Failed to change machine"); }); }); diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js index f18034df5..40b5022de 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/table.js @@ -33,14 +33,6 @@ function tableInit(ctx){ loadData(tableParams); - window.onpopstate = function(event){ - if (event.state){ - tableParams = event.state.tableParams; - /* We skip loadData and just update the table */ - updateTable(event.state.tableData); - } - }; - function loadData(tableParams){ $.ajax({ type: "GET", @@ -49,10 +41,8 @@ function tableInit(ctx){ headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function(tableData) { updateTable(tableData); - window.history.pushState({ - tableData: tableData, - tableParams: tableParams - }, null, libtoaster.dumpsUrlParams(tableParams)); + window.history.replaceState(null, null, + libtoaster.dumpsUrlParams(tableParams)); } }); } @@ -140,7 +130,7 @@ function tableInit(ctx){ tableBody.append(row); /* If we have layerbtns then initialise them */ - layerBtnsInit(ctx); + layerBtnsInit(); /* If we have popovers initialise them now */ $('td > a.btn').popover({ diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/tests/test.js b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/tests/test.js index d61011302..aac0ba60a 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/static/js/tests/test.js +++ b/yocto-poky/bitbake/lib/toaster/toastergui/static/js/tests/test.js @@ -13,8 +13,6 @@ QUnit.test("Layer alert notification", function(assert) { "name":"meta-example" }; - var correctResponse = "You have added <strong>3</strong> layers to your project: <a id=\"layer-affected-name\" href=\"/toastergui/project/1/layer/22\">meta-example</a> and its dependencies <a href=\"/toastergui/project/1/layer/9\" data-original-title=\"\" title=\"\">meta-example-two</a>, <a href=\"/toastergui/project/1/layer/9\" data-original-title=\"\" title=\"\">meta-example-three</a>"; - var layerDepsList = [ { "layerdetailurl":"/toastergui/project/1/layer/9", @@ -68,9 +66,9 @@ QUnit.test("Show notification", function(assert){ }); var layer = { - "id": 91, - "name": "meta-crystalforest", - "layerdetailurl": "/toastergui/project/4/layer/91" + "id": 1, + "name": "meta-testing", + "layerdetailurl": "/toastergui/project/1/layer/1" }; QUnit.test("Add layer", function(assert){ @@ -84,11 +82,19 @@ QUnit.test("Add layer", function(assert){ } }, 200); - libtoaster.addRmLayer(layer, true, function(deps){ - assert.equal(deps.length, 1); - done(); + /* Compare the number of layers before and after the add in the project */ + libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(prjInfo){ + var origNumLayers = prjInfo.layers.length; + + libtoaster.addRmLayer(layer, true, function(deps){ + libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, + function(prjInfo){ + assert.ok(prjInfo.layers.length > origNumLayers, + "Layer not added to project"); + done(); + }); + }); }); - }); QUnit.test("Rm layer", function(assert){ @@ -152,11 +158,11 @@ QUnit.test("Layer details page init", function(assert){ }); QUnit.test("Layer btns init", function(assert){ - assert.throws(layerBtnsInit({ projectLayers : [] })); + assert.throws(layerBtnsInit()); }); QUnit.test("Table init", function(assert){ - assert.throws(tableInit({ url : tableUrl })); + assert.throws(tableInit({ url : ctx.tableUrl })); }); $(document).ajaxError(function(event, jqxhr, settings, errMsg){ @@ -167,9 +173,3 @@ $(document).ajaxError(function(event, jqxhr, settings, errMsg){ assert.notOk(jqxhr.responseText); }); }); - - - - - - diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py index 92e3b5c66..9c9cda4e9 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/tables.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/tables.py @@ -21,6 +21,7 @@ from toastergui.widgets import ToasterTable from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project +from orm.models import CustomImageRecipe, Package from django.db.models import Q, Max from django.conf.urls import url from django.core.urlresolvers import reverse @@ -50,14 +51,13 @@ class LayersTable(ToasterTable): def __init__(self, *args, **kwargs): super(LayersTable, self).__init__(*args, **kwargs) self.default_orderby = "layer__name" + self.title = "Compatible layers" def get_context_data(self, **kwargs): context = super(LayersTable, self).get_context_data(**kwargs) project = Project.objects.get(pk=kwargs['pid']) - context['project'] = project - context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project)) return context @@ -91,7 +91,10 @@ class LayersTable(ToasterTable): def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) - compatible_layers = prj.compatible_layerversions() + compatible_layers = prj.get_all_compatible_layer_versions() + + self.static_context_extra['current_layers'] = \ + prj.get_project_layer_versions(pk=True) self.queryset = compatible_layers.order_by(self.default_orderby) @@ -208,6 +211,7 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin): def __init__(self, *args, **kwargs): super(MachinesTable, self).__init__(*args, **kwargs) self.empty_state = "No machines maybe you need to do a build?" + self.title = "Compatible machines" self.default_orderby = "name" def get_context_data(self, **kwargs): @@ -218,7 +222,7 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin): def setup_filters(self, *args, **kwargs): project = Project.objects.get(pk=kwargs['pid']) - self.project_layers = project.projectlayer_equivalent_set() + self.project_layers = project.get_project_layer_versions() self.add_filter(title="Filter by project machines", name="in_current_project", @@ -308,13 +312,20 @@ class LayerMachinesTable(MachinesTable): class RecipesTable(ToasterTable, ProjectFiltersMixin): - """Table of Recipes in Toaster""" + """Table of All Recipes in Toaster""" def __init__(self, *args, **kwargs): super(RecipesTable, self).__init__(*args, **kwargs) self.empty_state = "Toaster has no recipe information. To generate recipe information you can configure a layer source then run a build." self.default_orderby = "name" + build_col = { 'title' : "Build", + 'help_text' : "Add or delete recipes to and from your project", + 'hideable' : False, + 'filter_name' : "in_current_project", + 'static_data_name' : "add-del-layers", + 'static_data_template' : '{% include "recipe_btn.html" %}'} + def get_context_data(self, **kwargs): project = Project.objects.get(pk=kwargs['pid']) context = super(RecipesTable, self).get_context_data(**kwargs) @@ -326,9 +337,6 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): return context def setup_filters(self, *args, **kwargs): - project = Project.objects.get(pk=kwargs['pid']) - self.project_layers = project.projectlayer_equivalent_set() - self.add_filter(title="Filter by project recipes", name="in_current_project", filter_actions=[ @@ -336,24 +344,23 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project) ]) - def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) + # Project layers used by the filters + self.project_layers = prj.get_project_layer_versions(pk=True) + + # Project layers used to switch the button states + self.static_context_extra['current_layers'] = self.project_layers + self.queryset = prj.get_all_compatible_recipes() self.queryset = self.queryset.order_by(self.default_orderby) def setup_columns(self, *args, **kwargs): - self.add_column(title="Recipe", - help_text="Information about a single piece of software, including where to download the source, configuration options, how to compile the source files and how to package the compiled output", - hideable=False, - orderable=True, - field_name="name") - - self.add_column(title="Recipe Version", - hidden=True, + self.add_column(title="Version", + hidden=False, field_name="version") self.add_column(title="Description", @@ -374,6 +381,7 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): self.add_column(title="Section", help_text="The section in which recipes should be categorized", + hidden=True, orderable=True, field_name="section") @@ -390,24 +398,14 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin): self.add_column(title="License", help_text="The list of source licenses for the recipe. Multiple license names separated by the pipe character indicates a choice between licenses. Multiple license names separated by the ampersand character indicates multiple licenses exist that cover different parts of the source", + hidden=True, orderable=True, field_name="license") self.add_column(title="Revision", + hidden=True, field_name="layer_version__get_vcs_reference") - self.add_column(title="Build", - help_text="Add or delete recipes to and from your project", - hideable=False, - filter_name="in_current_project", - static_data_name="add-del-layers", - static_data_template='{% include "recipe_btn.html" %}') - - project = Project.objects.get(pk=kwargs['pid']) - self.add_column(title="Project compatible Layer ID", - displayable = False, - field_name = "projectcompatible_layer", - computation = lambda x: (x.layer_version.get_equivalents_wpriority(project)[0])) class LayerRecipesTable(RecipesTable): """ Smaller version of the Recipes table for use in layer details """ @@ -422,8 +420,8 @@ class LayerRecipesTable(RecipesTable): def setup_queryset(self, *args, **kwargs): - RecipesTable.setup_queryset(self, *args, **kwargs) - self.queryset = self.queryset.filter(layer_version__pk=int(kwargs['layerid'])) + self.queryset = \ + Recipe.objects.filter(layer_version__pk=int(kwargs['layerid'])) self.static_context_extra['in_prj'] = ProjectLayer.objects.filter(Q(project=kwargs['pid']) & Q(layercommit=kwargs['layerid'])).count() @@ -434,20 +432,197 @@ class LayerRecipesTable(RecipesTable): orderable=True, field_name="name") + self.add_column(title="Version", + field_name="version") + self.add_column(title="Description", field_name="get_description_or_summary") - build_recipe_template ='<button class="btn btn-block build-recipe-btn" data-recipe-name="{{data.name}}" {%if extra.in_prj == 0 %}disabled="disabled"{%endif%}>Build recipe</button>' self.add_column(title="Build recipe", static_data_name="add-del-layers", static_data_template=build_recipe_template) -class ProjectLayersRecipesTable(RecipesTable): - """ Table that lists only recipes available for layers added to the project """ +class CustomImagesTable(ToasterTable): + """ Table to display your custom images """ + def __init__(self, *args, **kwargs): + super(CustomImagesTable, self).__init__(*args, **kwargs) + self.title = "Custom images" + + def get_context_data(self, **kwargs): + context = super(CustomImagesTable, self).get_context_data(**kwargs) + project = Project.objects.get(pk=kwargs['pid']) + context['project'] = project + context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) + return context def setup_queryset(self, *args, **kwargs): - super(ProjectLayersRecipesTable, self).setup_queryset(*args, **kwargs) prj = Project.objects.get(pk = kwargs['pid']) - self.queryset = self.queryset.filter(layer_version__in = prj.projectlayer_equivalent_set()) + self.queryset = CustomImageRecipe.objects.filter(project=prj) + self.queryset = self.queryset.order_by('name') + + def setup_columns(self, *args, **kwargs): + + name_link_template = ''' + <a href="{% url 'customrecipe' extra.pid data.id %}"> + {{data.name}} + </a> + ''' + + self.add_column(title="Custom image", + hideable=False, + static_data_name="name", + static_data_template=name_link_template) + + self.add_column(title="Recipe file", + static_data_name='recipe_file', + static_data_template='') + + approx_packages_template = '<a href="#imagedetails">{{data.packages.all|length}}</a>' + self.add_column(title="Approx packages", + static_data_name='approx_packages', + static_data_template=approx_packages_template) + + + build_btn_template = '''<button data-recipe-name="{{data.name}}" + class="btn btn-block build-recipe-btn" style="margin-top: 5px;" > + Build</button>''' + + self.add_column(title="Build", + hideable=False, + static_data_name='build_custom_img', + static_data_template=build_btn_template) + +class ImageRecipesTable(RecipesTable): + """ A subset of the recipes table which displayed just image recipes """ + + def __init__(self, *args, **kwargs): + super(ImageRecipesTable, self).__init__(*args, **kwargs) + self.title = "Compatible image recipes" + + def setup_queryset(self, *args, **kwargs): + super(ImageRecipesTable, self).setup_queryset(*args, **kwargs) + + self.queryset = self.queryset.filter(is_image=True) + + + def setup_columns(self, *args, **kwargs): + self.add_column(title="Image recipe", + help_text="When you build an image recipe, you get an " + "image: a root file system you can" + "deploy to a machine", + hideable=False, + orderable=True, + field_name="name") + + super(ImageRecipesTable, self).setup_columns(*args, **kwargs) + + self.add_column(**RecipesTable.build_col) + + +class NewCustomImagesTable(ImageRecipesTable): + """ Table which displays Images recipes which can be customised """ + def __init__(self, *args, **kwargs): + super(NewCustomImagesTable, self).__init__(*args, **kwargs) + self.title = "Select the image recipe you want to customise" + + def setup_queryset(self, *args, **kwargs): + super(ImageRecipesTable, self).setup_queryset(*args, **kwargs) + + self.queryset = self.queryset.filter(is_image=True) + + def setup_columns(self, *args, **kwargs): + self.add_column(title="Image recipe", + help_text="When you build an image recipe, you get an " + "image: a root file system you can" + "deploy to a machine", + hideable=False, + orderable=True, + field_name="recipe__name") + + super(ImageRecipesTable, self).setup_columns(*args, **kwargs) + + self.add_column(title="Customise", + hideable=False, + filter_name="in_current_project", + static_data_name="customise-or-add-recipe", + static_data_template='{% include "customise_btn.html" %}') + + +class SoftwareRecipesTable(RecipesTable): + """ Displays just the software recipes """ + def __init__(self, *args, **kwargs): + super(SoftwareRecipesTable, self).__init__(*args, **kwargs) + self.title = "Compatible software recipes" + + def setup_queryset(self, *args, **kwargs): + super(SoftwareRecipesTable, self).setup_queryset(*args, **kwargs) + + self.queryset = self.queryset.filter(is_image=False) + + + def setup_columns(self, *args, **kwargs): + self.add_column(title="Software recipe", + help_text="Information about a single piece of " + "software, including where to download the source, " + "configuration options, how to compile the source " + "files and how to package the compiled output", + hideable=False, + orderable=True, + field_name="name") + + super(SoftwareRecipesTable, self).setup_columns(*args, **kwargs) + + self.add_column(**RecipesTable.build_col) + + +class SelectPackagesTable(ToasterTable): + """ Table to display the packages to add and remove from an image """ + + def __init__(self, *args, **kwargs): + super(SelectPackagesTable, self).__init__(*args, **kwargs) + self.title = "Add | Remove packages" + + def setup_queryset(self, *args, **kwargs): + cust_recipe = CustomImageRecipe.objects.get(pk=kwargs['recipeid']) + prj = Project.objects.get(pk = kwargs['pid']) + + current_packages = cust_recipe.packages.all() + + # Get all the packages that are in the custom image + # Get all the packages built by builds in the current project + # but not those ones that are already in the custom image + self.queryset = Package.objects.filter( + Q(pk__in=current_packages) | + (Q(build__project=prj) & + ~Q(name__in=current_packages.values_list('name')))) + + self.queryset = self.queryset.order_by('name') + + self.static_context_extra['recipe_id'] = kwargs['recipeid'] + self.static_context_extra['current_packages'] = \ + cust_recipe.packages.values_list('pk', flat=True) + + def setup_columns(self, *args, **kwargs): + self.add_column(title="Package", + hideable=False, + orderable=True, + field_name="name") + + self.add_column(title="Package Version", + field_name="version") + + self.add_column(title="Approx Size", + orderable=True, + static_data_name="size", + static_data_template="{% load projecttags %} \ + {{data.size|filtered_filesizeformat}}") + self.add_column(title="summary", + field_name="summary") + + self.add_column(title="Add | Remove", + help_text="Use the add and remove buttons to modify " + "the package content of you custom image", + static_data_name="add_rm_pkg_btn", + static_data_template='{% include "pkg_add_rm_btn.html" %}') diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html index 640bc47bc..11ac2a035 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/base.html @@ -1,9 +1,12 @@ <!DOCTYPE html> {% load static %} {% load projecttags %} +{% load project_url_tag %} <html lang="en"> <head> - <title>{% if objectname %} {{objectname|title}} - {% endif %}Toaster</title> + <title> + {% block title %} Toaster {% endblock %} + </title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/> <link rel="stylesheet" href="{% static 'css/bootstrap-responsive.min.css' %}" type='text/css'/> <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/> @@ -35,8 +38,9 @@ projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}}, {% if project.id %} projectId : {{project.id}}, - projectPageUrl : {% url 'project' project.id as purl%}{{purl|json}}, + projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}}, projectName : {{project.name|json}}, + projectIsDefault: {% if project.is_default %}true{% else %}false{% endif %}, recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}}, layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}}, machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}}, @@ -47,7 +51,7 @@ projectId : undefined, projectPageUrl : undefined, projectName : undefined, - projectId : undefined, + projectIsDefault: false, {% endif %} }; </script> @@ -89,9 +93,9 @@ <i class="icon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i> {% endif %} </span> - {% if request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %} + {% if BUILD_MODE and request.resolver_match.url_name != 'landing' and request.resolver_match.url_name != 'newproject' %} <ul class="nav"> - <li {% if request.resolver_match.url_name == 'all-builds' %} + <li {% if request.resolver_match.url_name == 'all-builds' %} class="active" {% endif %}> <a href="{% url 'all-builds' %}"> @@ -118,55 +122,65 @@ </li> </ul> <span class="pull-right divider-vertical"></span> - <div class="btn-group pull-right"> - <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a> - </div> - <!-- New build popover --> - <div class="btn-group pull-right" id="new-build-button" style="display:none"> - <button class="btn dropdown-toggle" data-toggle="dropdown"> - New build - <i class="icon-caret-down"></i> - </button> - <ul class="dropdown-menu new-build multi-select"> - <li> - <h3>New build</h3> - <h6>Project:</h6> - <span id="project"> - {% if project.id %} - <a class="lead" href="{% url 'project' project.id %}">{{project.name}}</a> - {% else %} - <a class="lead" href="#"></a> - {% endif %} - <i class="icon-pencil"></i> - </span> - <form id="change-project-form" style="display:none;"> - <div class="input-append"> - <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/> - <button id="save-project-button" class="btn" type="button">Save</button> - <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a> - </div> - <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p> - </form> - </li> - <li> - <div class="alert" style="display:none;"> - <p>This project configuration is incomplete, so you cannot run builds.</p> - <p><a href="{% if project.id %}{% url 'project' project.id %}{% endif %}">View project configuration</a></p> - </div> - </li> - <li id="targets-form"> - <h6>Recipe(s):</h6> - <form> - <input type="text" class="input-xlarge build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/> - <div class="row-fluid"> - <button class="btn btn-primary build-button" disabled>Build</button> - </div> - </form> - </li> - </ul> - </div> + <!-- new project button; only show in build mode --> + {% if BUILD_MODE %} + <div class="btn-group pull-right"> + <a class="btn" id="new-project-button" href="{% url 'newproject' %}">New project</a> + </div> + {% endif %} + <!-- + New build popover; only shown if there is at least one user-created project + and we're in build mode + --> + {% if BUILD_MODE and non_cli_projects.count > 0 %} + <div class="btn-group pull-right" id="new-build-button" style="display:none"> + <button class="btn dropdown-toggle" data-toggle="dropdown"> + New build + <i class="icon-caret-down"></i> + </button> + <ul class="dropdown-menu new-build multi-select"> + <li> + <h3>New build</h3> + <h6> + Project: + <span id="project"> + {% if project.id and not project.is_default %} + <a class="lead" href="{% project_url project %}">{{project.name}}</a> + {% else %} + <a class="lead" href="#"></a> + {% endif %} + <i class="icon-pencil"></i> + </span> + </h6> + <form id="change-project-form" style="display:none;"> + <div class="input-append"> + <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/> + <button id="save-project-button" class="btn" type="button">Save</button> + <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a> + </div> + <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p> + </form> + </li> + <li> + <div class="alert" style="display:none;"> + <p>This project configuration is incomplete, so you cannot run builds.</p> + <p><a href="{% if project.id %}{% url 'project' project.id %}{% endif %}">View project configuration</a></p> + </div> + </li> + <li id="targets-form"> + <h6>Recipe(s):</h6> + <form> + <input type="text" class="input-xlarge build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/> + <div class="row-fluid"> + <button class="btn btn-primary build-button" disabled>Build</button> + </div> + </form> + </li> + </ul> + </div> + {% endif %} </div> </div> </div> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html index 668e0bf5e..1f45be462 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html @@ -1,6 +1,9 @@ {% extends "base.html" %} {% load projecttags %} {% load humanize %} + +{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %} + {% block pagecontent %} {% include "projecttopbar.html" %} @@ -23,8 +26,11 @@ <ul class="nav nav-list well"> <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li> <li class="nav-header">Compatible metadata</li> -<!-- <li><a href="all-image-recipes.html">Image recipes</a></li> --> - <li><a href="{% url 'projecttargets' project.id %}">Recipes</a></li> + {% if CUSTOM_IMAGE %} + <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li> + {% endif %} + <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li> + <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li> <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li> <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li> <li class="nav-header">Extra configuration</li> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/bpackage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/bpackage.html index d775fec73..81973cbc6 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/bpackage.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/bpackage.html @@ -2,6 +2,7 @@ {% load projecttags %} +{% block title %} Packages built - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li>Packages</li> {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html index bab8e388f..323bbbb6e 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builddashboard.html @@ -2,8 +2,14 @@ {% load humanize %} {% load projecttags %} +{% block title %} {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block parentbreadcrumb %} -{{build.get_sorted_target_list.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}}) +{% if build.get_sorted_target_list.count > 0 %} + {{build.get_sorted_target_list.0.target}} + +{% endif %} + +{%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|date:"d/m/y H:i"}}) {% endblock %} {% block buildinfomain %} @@ -37,19 +43,13 @@ <span > <i class="icon-warning-sign yellow"></i><strong><a href="#warnings" class="warning show-warnings"> {{build.warnings.count}} warning{{build.warnings.count|pluralize}}</a></strong></span> {% endif %} <span class="pull-right">Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a> - <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%else%}btn-danger{%endif%} pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a> + {% if build.cooker_log_path %} + <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%else%}btn-danger{%endif%} pull-right log" href="{% url 'build_artifact' build.id "cookerlog" build.id %}">Download build log</a> + {% endif %} </span> {%endif%} </div> - {% if build.toaster_exceptions.count > 0 %} - <div class="row"> - <small class="pull-right"> - <i class="icon-question-sign get-help get-help-blue" title="" data-original-title="Toaster exceptions do not affect your build: only the operation of Toaster"></i> - <a class="show-exceptions" href="#exceptions">Toaster threw {{build.toaster_exceptions.count}} exception{{build.toaster_exceptions.count|pluralize}}</a> - </small> - </div> - {% endif %} </div> </div> @@ -67,11 +67,10 @@ <div class="accordion-body collapse in" id="collapse-errors"> <div class="accordion-inner"> <div class="span10"> - {% for error in logmessages %}{% if error.level == 2 %} - <div class="alert alert-error"> + {% for error in build.errors %} + <div class="alert alert-error" data-error="{{ error.id }}"> <pre>{{error.message}}</pre> </div> - {% endif %} {% endfor %} </div> </div> @@ -268,33 +267,6 @@ </div> {% endif %} - -{% if build.toaster_exceptions.count > 0 %} -<div class="accordion span10 pull-right" id="exceptions"> - <div class="accordion-group"> - <div class="accordion-heading"> - <a class="accordion-toggle exception toggle-exceptions"> - <h2 id="exception-toggle"> - <i class="icon-warning-sign"></i> - {{build.toaster_exceptions.count}} Toaster exception{{build.toaster_exceptions.count|pluralize}} - </h2> - </a> - </div> - <div class="accordion-body collapse" id="collapse-exceptions"> - <div class="accordion-inner"> - <div class="span10"> - {% for exception in build.toaster_exceptions %} - <div class="alert alert-exception"> - <pre>{{exception.message}}</pre> - </div> - {% endfor %} - </div> - </div> - </div> - </div> -</div> -{% endif %} - <script type="text/javascript"> $(document).ready(function() { //show warnings section when requested from the previous page diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html index c0d0c64c2..a27a12191 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/builds.html @@ -2,8 +2,10 @@ {% load static %} {% load projecttags %} +{% load project_url_tag %} {% load humanize %} +{% block title %} All builds - Toaster {% endblock %} {% block extraheadcontent %} <link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'> <link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'> @@ -28,8 +30,6 @@ {% include "mrb_section.html" %} - - {% if 1 %} <div class="page-header top-air"> <h1> {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %} @@ -56,17 +56,25 @@ </form> </div> </div> - - {% else %} {% include "basetable_top.html" %} <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> {% for build in objects %} - <tr class="data"> + <tr class="data" data-table-build-result="{{ build.id }}"> <td class="outcome"> - <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> - </td> - <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> + <a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a> + </td> + <td class="target"> + {% for t in build.target_set.all %} + <a href="{% url "builddashboard" build.id %}"> + {% if t.task %} + {{t.target}}:{{t.task}} + {% else %} + {{t.target}} + {% endif %} + </a> <br /> + {% endfor %} + </td> <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td> @@ -93,8 +101,11 @@ <a href="{%url "builddashboard" build.id%}#images">{{fstypes|get_dict_value:build.id}}</a> {% endif %} </td> - <td> - <a href="{% url 'project' build.project.id %}">{{build.project.name}}</a> + <td class="project-name"> + <a href="{% project_url build.project %}">{{build.project.name}}</a> + {% if build.project.is_default %} + <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i> + {% endif %} </td> </tr> @@ -103,7 +114,6 @@ {% include "basetable_bottom.html" %} {% endif %} {# objects.paginator.count #} -{% endif %} {# empty #} </div><!-- end row-fluid--> {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/configuration.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/configuration.html index 3e489918d..85d6a622a 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/configuration.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/configuration.html @@ -1,6 +1,7 @@ {% extends "basebuildpage.html" %} {% load projecttags %} +{% block title %} Configuration summary - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li>Configuration</li> {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/configvars.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/configvars.html index 8a572ae39..e40c225a3 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/configvars.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/configvars.html @@ -1,6 +1,7 @@ {% extends "basebuildpage.html" %} {% load projecttags %} +{% block title %} BitBake variables - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li>Configuration</li> {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html new file mode 100644 index 000000000..54d05f9ea --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customise_btn.html @@ -0,0 +1,9 @@ +<button class="btn btn-block layer-exists-{{data.layer_version.id}} customise-btn" style="display:none;" data-recipe="{{data.id}}"> + Customise +</button> + +<button class="btn btn-block layer-add-{{data.layer_version.id}} layerbtn" data-layer='{ "id": {{data.layer_version.id}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{% url 'layerdetails' extra.pid data.layer_version.id %}"}' data-directive="add"> + <i class="icon-plus"></i> + Add layer +</button> + diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html new file mode 100644 index 000000000..823bbd8a1 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/customrecipe.html @@ -0,0 +1,142 @@ +{% extends "base.html" %} +{% load projecttags %} +{% load humanize %} +{% load static %} +{% block pagecontent %} + +{% include "projecttopbar.html" %} + +<script src="{% static 'js/customrecipe.js' %}"></script> +<script> + $(document).ready(function (){ + var ctx = { + tableApiUrl: "{% url 'recipeselectpackages' project.id recipe.pk %}?format=json" + }; + + try { + customRecipePageInit(ctx); + } catch (e) { + document.write("Sorry, An error has occurred loading this page"); + console.warn(e); + } + }); +</script> + +<div class="row-fluid span11"> + <div class="alert alert-success lead" id="image-created-notification" style="margin-top: 15px; display: none"> + <button type="button" data-dismiss="alert" class="close">x</button> + Your custom image <strong>{{recipe.name}}</strong> has been created. You can now add or remove packages as needed. + </div> + <div class="page-header air"> + <h1> + {{recipe.name}} + <small>({{recipe.base_recipe.name}})</small> + </h1> + </div> +</div> + +<div class="row-fluid span11"> + <div class="span8"> + <div class="button-place btn-group" style="width: 100%"> + <a class="btn btn-large span6" href="#" id="build-custom-image" style="width: 50%"> + Build {{recipe.name}} + </a> + <button class="btn btn-large span6" data-toggle="modal" data-target="#download-file" id="download" style="width: 50%"> + Download recipe file + </button> + </div> + <div id="no-package-results" class="air" style="display:none;"> + <div class="alert"> + <h3>No packages found</h3> + <p>You might consider <a href="all-software-recipes.html">searching the list of recipes</a> instead. If you find a recipe that matches the name of the package you want:</p> + <ol> + <li>Add the layer providing the recipe to your project</li> + <li>Build the recipe</li> + <li>Once the build completes, come back to this page and search for the package</li> + </ol> + <form class="input-append no-results"> + <input type="text" class="input-xlarge" value="search query"> + <a href="#" class="add-on btn"> + <i class="icon-remove"></i> + </a> + <button class="btn">Search</button> + <button class="btn btn-link" id="show-all">Show all packages</button> + </form> + </div> + </div> + <div id="packages-table"> + {% url 'recipeselectpackages' project.id recipe.id as xhr_table_url %} + {% with 'recipeselection' as table_name %} + {% with 'Add | Remove packages' as title %} + + <h2>{{title}} (<span class="table-count-{{table_name}}"></span>) </h2> + + {% include "toastertable.html" %} + {% endwith %} + {% endwith %} + </div> + </div> + <div class="span4 well"> + <h2 style="margin-bottom:20px;">About {{recipe.name}}</h2> + + <dl> + <dt> + Approx. packages included + <i class="icon-question-sign get-help" title="" data-original-title="The number of packages included is based on information from previous builds and from parsing layers, so we can never be sure it is 100% accurate"></i> + </dt> + <dd class="no-packages">{{recipe.packages.count}}</dd> + <!-- <dt> + Approx. package size + <i class="icon-question-sign get-help" title="" data-original-title="Package size is based on information from previous builds, so we can never be sure it is 100% accurate"></i> + </dt> + <dd>244.3 MB</dd> + <dt>Last build</dt> + <dd> + <i class="icon-ok-sign success"></i> + <a href="build-dashboard.html">11/06/15 15:22</a> + </dd> + <dt>Recipe file</dt> + <dd> + <code>custom-image-name.bb</code> + <a href="#download-file" data-toggle="modal"><i class="icon-download-alt" title="" data-original-title="Download recipe file"></i></a> + </dd> --> + <dt>Layer</dt> + <!-- TODO recipe details page --> + <dd><a href="{% url 'layerdetails' project.id recipe.base_recipe.layer_version.pk %}">{{recipe.base_recipe.layer_version.layer.name}}</a></dd> + <!--<dt> + Summary + </dt> + <dd> + <span class="muted">Not set</span> + <i class="icon-pencil" data-original-title="" title=""></i> + </dd> + <dt> + Description + </dt> + <dd> + <span class="muted">Not set</span> + <i class="icon-pencil" data-original-title="" title=""></i> + </dd> + <dt>Version</dt> + <dd> + 1.0 + <i class="icon-pencil" data-original-title="" title=""></i> + </dd> + <dt>Section</dt> + <dd> + base + <i class="icon-pencil" data-original-title="" title=""></i> + <i class="icon-trash" data-original-title="" title=""></i> + </dd> + <dt>License</dt> + <dd> + MIT + <i class="icon-question-sign get-help" title="" data-original-title="All custom images have their license set to MIT. This is because the license applies only to the recipe (.bb) file, and not to the image itself. To see which licenses apply to the image you must check the license manifest generated with each build"></i> + </dd> --> + </dl> + <i class="icon-trash no-tooltip"></i> + <a href="#" class="error" id="delete">Delete custom image</a> + </div> +</div> + + {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/dirinfo.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/dirinfo.html index a5bc48127..ecb46bf7a 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/dirinfo.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/dirinfo.html @@ -1,4 +1,5 @@ {% extends "basebuildpage.html" %} +{% block title %} Directory structure - {{ target.target }} {{ build.machine }} - {{ build.project.name }} - Toaster {% endblock %} {% block extraheadcontent %} {% load static %} <link rel="stylesheet" href="{% static 'css/jquery.treetable.css' %}" type="text/css"> @@ -103,12 +104,16 @@ name += '</td>'; } else { - name = '<td>'; if (o.link_to == null) { - name += '<i class="icon-file"></i>'; + namespan = 2; + if (o.package == null) { + namespan = 3; + } + var colspan = 'colspan="' + namespan + '"'; + name = '<td ' + colspan + '><i class="icon-file"></i>'; } else { - name += '<i class="icon-hand-right"></i>'; + name = '<td><i class="icon-hand-right"></i>'; } name += ' ' + o.name; name += '</td>'; @@ -207,10 +212,10 @@ <th>Directory / File</th> <th>Symbolic link to</th> <th>Source package</th> - <th>Size</th> - <th>Permissions</th> - <th>Owner</th> - <th>Group</th> + <th class="narrow-col">Size</th> + <th class="medium-col">Permissions</th> + <th class="narrow-col">Owner</th> + <th class="narrow-col">Group</th> </tr> </thead> <tbody> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html index ce3d724c8..033f0aede 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/importlayer.html @@ -2,6 +2,7 @@ {% load projecttags %} {% load humanize %} {% load static %} +{% block title %} Import layer - {{project.name}} - Toaster {% endblock %} {% block pagecontent %} {% include "projecttopbar.html" %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html index 5b8fd8447..8d65f33cb 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/js-unit-tests.html @@ -22,9 +22,11 @@ <script src="{% static 'js/table.js' %}"></script> <script> - var tableUrl = '{% url 'projectlayers' project.pk %}'; + var ctx = { + tableUrl : '{% url 'projectlayers' project.pk %}', + projectId : {{project.pk}}, + } </script> - <script src="{% static 'js/tests/test.js' %}"></script> <div id="qunit"></div> @@ -34,6 +36,12 @@ <input type="text" id="projects" placeholder="projects"></input> <input type="text" id="machines" placeholder="machines"></input> -{% endblock %} - +<!-- import layer dependency input typeahead --> +<input type="text" id="layer-dependency" style="display:none"></input> +<!-- project page input typeaheads --> +<input type="text" id="layer-add-input" style="display:none"></input> +<input type="text" id="machine-change-input" style="display:none"></input> +<!-- import layer dependency input typeahead on layer details edit layer --> +<input type="text" id="layer-dep-input" style="display:none"></input> +{% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html index 45e95322d..cafaa1afa 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing.html @@ -4,55 +4,69 @@ {% load projecttags %} {% load humanize %} +{% block title %} Welcome to Toaster {% endblock %} {% block pagecontent %} - <div class="container-fluid"> - <div class="row-fluid"> - <!-- Empty - no data in database --> - <div class="hero-unit span12 well-transparent"> - <div class="row-fluid"> - <div class="span6"> - <h1> - This is Toaster - </h1> - <p>A web interface to <a href="http://www.openembedded.org">OpenEmbedded</a> and <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake">BitBake</a>, the <a href="http://www.yoctoproject.org">Yocto Project</a> build system.</p> - - - {% if lvs_nos %} - <p class="hero-actions"> - <a class="btn btn-primary btn-large" href="{% url 'newproject' %}"> - To start building, create your first Toaster project - </a> - </p> - {% else %} - <div class="alert alert-info lead air"> - Toaster has no layer information. Without layer information, you cannot run builds. To generate layer information you can: - <ul> - <li> - <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html#layer-source">Configure a layer source</a> - </li> - <li> - <a href="{% url 'newproject' %}">Create a project</a>, then import layers - </li> - </ul> - </div> - {% endif %} - - <ul class="unstyled"> - <li> - <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html">Read the Toaster manual</a> - </li> - <li> - <a href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster">Contribute to Toaster</a> - </li> - </ul> + {% if BUILD_MODE %} + <!-- build mode --> + <div class="container-fluid"> + <div class="row-fluid"> + <div class="hero-unit span12 well-transparent"> + <div class="row-fluid"> + <div class="span6"> + <h1>This is Toaster</h1> + + <p>A web interface to <a href="http://www.openembedded.org">OpenEmbedded</a> and <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake">BitBake</a>, the <a href="http://www.yoctoproject.org">Yocto Project</a> build system.</p> + + {% if lvs_nos %} + <p class="hero-actions"> + <a class="btn btn-primary btn-large" href="{% url 'newproject' %}"> + To start building, create your first Toaster project + </a> + </p> + {% else %} + <div class="alert alert-info lead air"> + Toaster has no layer information. Without layer information, you cannot run builds. To generate layer information you can: + <ul> + <li> + <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html#layer-source">Configure a layer source</a> + </li> + <li> + <a href="{% url 'newproject' %}">Create a project</a>, then import layers + </li> + </ul> + </div> + {% endif %} + + <ul class="unstyled"> + <li> + <a href="http://www.yoctoproject.org/docs/latest/toaster-manual/toaster-manual.html"> + Read the Toaster manual + </a> + </li> + + <li> + <a href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster"> + Contribute to Toaster + </a> + </li> + </ul> + </div> + + <div class="span6"> + <img alt="Yocto Project" class="thumbnail" src="{% static 'img/toaster_bw.png' %}"/> + </div> + + </div> + </div> </div> - <div class="span6"> - <img alt="Yocto Project" class="thumbnail" src="{% static 'img/toaster_bw.png' %}"/> - </div> - </div> </div> - </div> + {% else %} + <!-- analysis mode --> + <div class="alert alert-info lead top-air"> + Toaster has not recorded any builds yet. Run a build from the command line to see it here. + </div> + {% endif %} {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html index 5bc435d14..9b37f5530 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/landing_not_managed.html @@ -4,6 +4,8 @@ {% load projecttags %} {% load humanize %} +{% block title %} Welcome to Toaster {% endblock %} + {% block pagecontent %} <div class="container-fluid"> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html index a2e93934d..314eec7cf 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layer_btn.html @@ -1,8 +1,16 @@ -<button class="btn btn-danger btn-block layer-exists-{{data.pk}} layerbtn" style="display:none;" data-layer='{ "id": {{data.pk}}, "name": "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="remove" > +<button class="btn btn-danger btn-block layer-exists-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name": "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="remove" + {% if data.pk not in extra.current_layers %} + style="display:none;" + {% endif %} + > <i class="icon-trash"></i> Delete layer </button> -<button class="btn btn-block layer-add-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name": "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="add"> +<button class="btn btn-block layer-add-{{data.pk}} layerbtn" data-layer='{ "id": {{data.pk}}, "name": "{{data.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.pk%}"}' data-directive="add" + {% if data.pk in extra.current_layers %} + style="display:none;" + {% endif %} + > <i class="icon-plus"></i> Add layer </button> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html index 7dd3db27a..7fe365da3 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/layerdetails.html @@ -3,6 +3,7 @@ {% load humanize %} {% load static %} +{% block title %} {{layerversion.layer.name}} - {{project.name}} - Toaster {% endblock %} {% block pagecontent %} <div class="section"> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html index 396fb8edf..bd8f99178 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/mrb_section.html @@ -1,43 +1,68 @@ {% load static %} {% load projecttags %} +{% load project_url_tag %} {% load humanize %} +{%if mru and mru.count > 0%} -{%if mru.count > 0%} + {%if mrb_type == 'project' %} + <h2> + Latest project builds - <div class="page-header"> + {% if project.is_default %} + <i class="icon-question-sign get-help heading-help" title="" data-original-title="Builds in this project cannot be started from Toaster: they are started from the command line"></i> + {% endif %} + </h2> + {% else %} + <div class="page-header"> <h1> - Latest builds - </h1> - </div> + Latest builds + </h1> + </div> + {% endif %} <div id="latest-builds"> {% for build in mru %} - <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%} project-name "> - <span class="label {%if build.outcome == build.SUCCEEDED%}label-success{%elif build.outcome == build.FAILED%}label-important{%else%}label-info{%endif%}"> - <a href={% url 'project' build.project.pk %}> - {{build.project.name}} - </a> - </span> - + <div data-latest-build-result="{{ build.id }}" class="alert build-result {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}{% if mrb_type != 'project' %} project-name{% endif %}"> + {% if mrb_type != 'project' %} + <span class="label {%if build.outcome == build.SUCCEEDED%}label-success{%elif build.outcome == build.FAILED%}label-important{%else%}label-info{%endif%}"> + <a href={% project_url build.project %}> + {{build.project.name}} + </a> + </span> + {% endif %} <div class="row-fluid"> - <div class="span3 lead"> + <div class="span3 lead"> {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} - <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}"> + <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}"> {% endif %} {% if build.target_set.all.count > 0 %} <span data-toggle="tooltip" - {%if build.target_set.all.count > 1%} - title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}" - {%endif%} + {% if build.target_set.all.count > 1 %} + title="Targets: + {% for target in build.target_set.all %} + {% if target.task %} + {{target.target}}:{{target.task}} + {% else %} + {{target.target}} + {% endif %} + {% endfor %}" + {% endif %} > - - {{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} + {% if build.target_set.all.0.task %} + {{build.target_set.all.0.target}}:{{build.target_set.all.0.task}} + {% else %} + {{build.target_set.all.0.target}} + {% endif %} + {% if build.target_set.all.count > 1 %} + (+ {{build.target_set.all.count|add:"-1"}}) + {% endif %} </span> {% endif %} {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} </a> {% endif %} </div> + {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} <div class="span2 lead"> {% if build.completed_on|format_build_date %} {{ build.completed_on|date:'d/m/y H:i' }} @@ -45,6 +70,7 @@ {{ build.completed_on|date:'H:i' }} {% endif %} </div> + {% endif %} {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} <div class="span2 lead"> {% if build.errors.count %} @@ -58,28 +84,41 @@ </div> <div class="lead "> <span class="lead"> - Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a> + Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}</a> </span> - <button class="btn + {% if build.project.is_default %} + <i class="pull-right icon-question-sign get-help {% if build.outcome == build.SUCCEEDED %} - btn-success + get-help-green {% elif build.outcome == build.FAILED %} - btn-danger + get-help-red {% else %} - btn-info - {%endif%} - pull-right" - onclick='scheduleBuild({% url 'projectbuilds' build.project.id as bpi %}{{bpi|json}}, - {{build.project.name|json}}, - {% url 'project' build.project.id as bpurl %}{{bpurl|json}}, - {{build.target_set.all|get_tasks|json}})'> + get-help-blue + {% endif %} + " title="Builds in this project cannot be started from Toaster: they are started from the command line"> + </i> + {% else %} + <button class="btn + {% if build.outcome == build.SUCCEEDED %} + btn-success + {% elif build.outcome == build.FAILED %} + btn-danger + {% else %} + btn-info + {%endif%} + pull-right" + onclick='scheduleBuild({% url 'projectbuilds' build.project.id as bpi %}{{bpi|json}}, + {{build.project.name|json}}, + {% url 'project' build.project.id as purl %}{{purl|json}}, + {{build.target_set.all|get_tasks|json}})'> - Run again - </button> + Run again + </button> + {% endif %} </div> {%endif%} {%if build.outcome == build.IN_PROGRESS %} - <div class="span4"> + <div class="span4 offset1"> <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete"> <div style="width: {{build.completeper}}%;" class="bar"></div> </div> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html new file mode 100644 index 000000000..4487b3ea0 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newcustomimage.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% load projecttags %} +{% load humanize %} +{% load static %} +{% block pagecontent %} + +<script src="{% static 'js/newcustomimage.js' %}"></script> +<script> + $(document).ready(function (){ + var ctx = { + xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}", + }; + + try { + newCustomImagePageInit(ctx); + } catch (e) { + document.write("Sorry, An error has occurred loading this page"); + console.warn(e); + } + }); +</script> + +</script> +<div class="modal hide fade in" id="new-custom-image-modal" aria-hidden="false"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Name your custom image</h3> + </div> + <div class="modal-body"> + <div class="row-fluid"> + <span class="help-block span8">Image names must be unique. They should not contain spaces or capital letters, and the only allowed special character is dash (-).<p></p> + </span></div> + <div class="control-group controls"> + <input type="text" class="huge span5" placeholder="Type the name, something like 'core-image-myimage'"> + <span class="help-block" style="display:none">Image names cannot contain spaces or capital letters. The only allowed special character is dash (-)</span> + <span class="help-block" style="display: none">An image with this name already exists. Image names must be unique: try a different one.</span> + </div> + </div> + <div class="modal-footer"> + <a href="#" id="create-new-custom-image-btn" class="btn btn-primary btn-large" data-original-title="" title="">Create custom image</a> + </div> +</div> + +{% include "projecttopbar.html" %} + + +{% url table_name project.id as xhr_table_url %} +{% include "toastertable.html" %} + + + +{% endblock %} + + diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newproject.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newproject.html index 997390bcf..e83b2bea6 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/newproject.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/newproject.html @@ -1,6 +1,9 @@ {% extends "base.html" %} {% load projecttags %} {% load humanize %} + +{% block title %} Create a new project - Toaster {% endblock %} + {% block pagecontent %} <div class="row-fluid"> <div class="page-header"> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_detail_base.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_detail_base.html index a24bc8e43..9fa28a8f8 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_detail_base.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_detail_base.html @@ -1,6 +1,13 @@ {% extends "basebuilddetailpage.html" %} {% load projecttags %} +{% block title %} + {% if target %} + {{package.fullpackagespec}} - {{ target.target }} {{ build.machine }} - {{ build.project.name }} - Toaster + {% else %} + {{package.fullpackagespec}} - {{ build.target_set.all|dictsort:"target"|join:", " }} {{ build.machine }} - {{ build.project.name }} - Toaster + {% endif %} +{% endblock %} {% block extraheadcontent %} <!-- functions to format package 'installed_package' alias --> <script> @@ -38,9 +45,9 @@ {% block pagedetailinfomain %} <div class="row span11"> <div class="page-header"> - {% block title %} + {% block mainheading %} <h1>{{package.fullpackagespec}}</h1> - {% endblock title %} + {% endblock %} </div> <!-- page-header --> </div> <!-- row span11 page-header --> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html index 642ca6956..8a0508e70 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_dependencies.html @@ -1,13 +1,13 @@ {% extends "package_detail_base.html" %} {% load projecttags %} -{% block title %} +{% block mainheading %} <h1> {{package.fullpackagespec}} <script> fmtAliasHelp("{{package.name}}", "{{package.alias}}", false) </script> <small>({{target.target}})</small> </h1> -{% endblock title %} +{% endblock %} {% block tabcontent %} {% with packageFileCount=package.buildfilelist_package.count %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_detail.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_detail.html index d2aa26eef..568e2f235 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_detail.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_detail.html @@ -1,7 +1,7 @@ {% extends "package_detail_base.html" %} {% load projecttags %} -{% block title %} +{% block mainheading %} <h1> {{package.fullpackagespec}} <script> @@ -9,7 +9,7 @@ </script> <small>({{target.target}})</small> </h1> -{% endblock title %} +{% endblock %} {% block tabcontent %} {% with packageFileCount=package.buildfilelist_package.count %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html index 5cc8b47a6..fb310c7fc 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/package_included_reverse_dependencies.html @@ -1,13 +1,13 @@ {% extends "package_detail_base.html" %} {% load projecttags %} -{% block title %} +{% block mainheading %} <h1> {{package.fullpackagespec}} <script> fmtAliasHelp("{{package.name}}", "{{package.alias}}", false) </script> <small>({{target.target}})</small> </h1> -{% endblock title %} +{% endblock %} {% block tabcontent %} {% with packageFileCount=package.buildfilelist_package.count %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html new file mode 100644 index 000000000..b766aeac9 --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/pkg_add_rm_btn.html @@ -0,0 +1,16 @@ +<button class="btn btn-block btn-danger add-rm-package-btn" id="package-rm-btn-{{data.pk}}" data-directive="remove" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style=" + {% if data.pk not in extra.current_packages %} + display:none + {% endif %} + "> + <i class="icon-trash no-tooltip"></i> + Remove package +</a> +<button class="btn btn-block add-rm-package-btn" data-directive="add" id="package-add-btn-{{data.pk}}" data-package="{{data.pk}}" data-package-url="{% url 'xhr_customrecipe_packages' extra.recipe_id data.pk %}" style=" + {% if data.pk in extra.current_packages %} + display:none + {% endif %} + "> +<i class="icon-plus"></i> + Add package +</button> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html index e8354fd67..4e83981f8 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/project.html @@ -4,6 +4,7 @@ {% load humanize %} {% load static %} +{% block title %} Configuration - {{project.name}} - Toaster {% endblock %} {% block projectinfomain %} <script src="{% static 'js/layerDepsModal.js' %}"></script> @@ -67,7 +68,7 @@ <div class="alert alert-info" style="display:none" id="no-most-built"> <span class="lead">You haven't built any recipes yet</span> - <p style="margin-top: 10px;"><a href="{% url 'projecttargets' project.id %}">Choose a recipe to build</a></p> + <p style="margin-top: 10px;"><a href="{% url 'projectsoftwarerecipes' project.id %}">Choose a recipe to build</a></p> </div> <ul class="unstyled configuration-list" id="freq-build-list"> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html index 27cfcd7dc..bb38284aa 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectbuilds.html @@ -2,7 +2,7 @@ {% load projecttags %} {% load humanize %} - +{% block title %} Builds - {{project.name}} - Toaster {% endblock %} {% block extraheadcontent %} <link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'> <link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'> @@ -21,13 +21,17 @@ }); </script> + {% with mrb_type='project' %} + {% include "mrb_section.html" %} + {% endwith %} + <h2> {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %} - {{objects.paginator.count}} build{{objects.paginator.count|pluralize}} found + {{objects.paginator.count}} project build{{objects.paginator.count|pluralize}} found {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %} - No builds found + No project builds found {%else%} - Project builds + All project builds {%endif%} <i class="icon-question-sign get-help heading-help" title="This page lists all the builds for the current project"></i> </h2> @@ -66,7 +70,17 @@ {% endif %} </td> - <td class="target">{% for t in build.target_set.all %} <a href="{% url "builddashboard" build.id %}"> {{t.target}} </a> <br />{% endfor %}</td> + <td class="target"> + {% for t in build.target_set.all %} + <a href="{% url "builddashboard" build.id %}"> + {% if t.task %} + {{t.target}}:{{t.task}} + {% else %} + {{t.target}} + {% endif %} + </a> <br /> + {% endfor %} + </td> <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td> <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html index 4c5a188a8..30fd03e32 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projectconf.html @@ -2,7 +2,7 @@ {% load projecttags %} {% load humanize %} - +{% block title %} BitBake variables - {{project.name}} - Toaster {% endblock %} {% block projectinfomain %} <h2>Bitbake variables</h2> @@ -43,6 +43,7 @@ <input id="filter-image_fstypes" type="text" placeholder="Search image types" class="span4"> <div id="all-image_fstypes" class="scrolling"> </div> + <span class="help-block" id="fstypes-error-message">You must select at least one image type</span> <button id="apply-change-image_fstypes" type="button" class="btn">Save</button> <button id="cancel-change-image_fstypes" type="button" class="btn btn-link">Cancel</button> </form> @@ -312,9 +313,11 @@ }); if ( 0 == any_checked ) { $("#apply-change-image_fstypes").attr("disabled","disabled"); + $('#fstypes-error-message').show(); } else { $("#apply-change-image_fstypes").removeAttr("disabled"); + $('#fstypes-error-message').hide(); } } @@ -546,10 +549,14 @@ // Add the un-checked boxes second for (var i = 0, length = fstypes_list.length; i < length; i++) { if (0 > fstypes.indexOf(" "+fstypes_list[i].value+" ")) { - html += '<label class="checkbox"><input type="checkbox" class="fs-checkbox-fstypes" value="'+fstypes_list[i].value+'">'+fstypes_list[i].value+'</label>\n'; + html += '<label class="checkbox"><input type="checkbox" class="fs-checkbox-fstypes" value="'+fstypes_list[i].value+'">'+fstypes_list[i].value+'</label>\n'; } } + // Add the 'no search matches' line last + html += '<label id="no-match-fstypes">No image types found</label>\n'; + // Display the list document.getElementById("all-image_fstypes").innerHTML = html; + $('#no-match-fstypes').hide(); // Watch elements to disable Save when none are checked $(".fs-checkbox-fstypes").each(function(){ @@ -558,8 +565,9 @@ }); }); - // clear the previous filter values + // clear the previous filter values and warning messages $("input#filter-image_fstypes").val(""); + $('#fstypes-error-message').hide(); }); $('#cancel-change-image_fstypes').click(function(){ @@ -569,17 +577,24 @@ }); $('#filter-image_fstypes').on('input', function(){ - var valThis = $(this).val().toLowerCase(); + var valThis = $(this).val().toLowerCase(); + var matchCount=0; $('#all-image_fstypes label').each(function(){ var text = $(this).text().toLowerCase(); var match = text.indexOf(valThis); if (match >= 0) { $(this).show(); + matchCount += 1; } else { $(this).hide(); } }); + if (matchCount === 0) { + $('#no-match-fstypes').show(); + } else { + $('#no-match-fstypes').hide(); + } }); $('#apply-change-image_fstypes').click(function(){ diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html index c2d77b5a3..678a7963b 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projects.html @@ -2,8 +2,11 @@ {% load static %} {% load projecttags %} +{% load project_url_tag %} {% load humanize %} +{% block title %} All projects - Toaster {% endblock %} + {% block pagecontent %} @@ -36,17 +39,29 @@ {% else %} {# We have builds to display #} {% include "basetable_top.html" %} {% for o in objects %} - <tr class="data"> - <td><a href="{% url 'project' o.id %}">{{o.name}}</a></td> - <td class="updated"><a href="{% url 'project' o.id %}">{{o.updated|date:"d/m/y H:i"}}</a></td> - <td> + <tr class="data" data-project="{{ o.id }}"> + <td data-project-field="name"> + <a href="{% project_url o %}">{{o.name}}</a> + </td> + <td class="updated"><a href="{% project_url o %}">{{o.updated|date:"d/m/y H:i"}}</a></td> + <td data-project-field="release"> {% if o.release %} <a href="{% url 'project' o.id %}#project-details">{{o.release.name}}</a> + {% elif o.is_default %} + <span class="muted">Not applicable</span> + <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a release set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i> {% else %} No release available {% endif %} </td> - <td><a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a></td> + <td data-project-field="machine"> + {% if o.is_default %} + <span class="muted">Not applicable</span> + <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a machine set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i> + {% else %} + <a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a> + {% endif %} + </td> {% if o.get_number_of_builds == 0 %} <td class="muted">{{o.get_number_of_builds}}</td> <td class="loutcome"></td> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html index ca2741daf..ee86b5481 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/projecttopbar.html @@ -1,12 +1,18 @@ <div class="alert alert-success lead" id="project-created-notification" style="margin-top:15px; display:none"> <button type="button" class="close" data-dismiss="alert">×</button> - Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projecttargets' project.id %}">choose image recipes</a> to build. + Your project <strong>{{project.name}}</strong> has been created. You can now <a href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a href="{% url 'projectsoftwarerecipes' project.id %}">choose image recipes</a> to build. </div> <!-- project name --> <div class="page-header"> - <h1><span id="project-name">{{project.name}}</span> + <h1 id="project-name-container"> + <span id="project-name">{{project.name}}</span> + <i class="icon-pencil" data-original-title="" id="project-change-form-toggle" title=""></i> + + {% if project.is_default %} + <i class="icon-question-sign get-help heading-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running"></i> + {% endif %} </h1> <form id="project-name-change-form" style="margin-bottom: 0px; display: none;"> <div class="input-append"> @@ -17,31 +23,40 @@ </form> </div> -<div id="project-topbar"> - <ul class="nav nav-pills"> - <li> - <a href="{% url 'projectbuilds' project.id %}"> - Builds (<span class="total-builds">0</span>) - </a> - </li> - <li id="topbar-configuration-tab"> - <a href="{% url 'project' project.id %}"> - Configuration - </a> - </li> - <li> - <a href="{% url 'importlayer' project.id %}"> - Import layer - </a> - </li> - <li class="pull-right"> - <form class="form-inline" style="margin-bottom:0px;"> - <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a semicolon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i> - <div class="input-append"> - <input id="build-input" type="text" class="input-xlarge input-lg build-target-input" placeholder="Type the recipe you want to build" autocomplete="off" disabled> - <button id="build-button" class="btn btn-primary btn-large build-button" data-project-id="{{project.id}}" disabled>Build</button> - </div> - </form> - </li> - </ul> -</div> +{% if not project.is_default %} + <div id="project-topbar"> + <ul class="nav nav-pills"> + <li> + <a href="{% url 'projectbuilds' project.id %}"> + Builds (<span class="total-builds">0</span>) + </a> + </li> + <li id="topbar-configuration-tab"> + <a href="{% url 'project' project.id %}"> + Configuration + </a> + </li> + <li> + <a href="{% url 'importlayer' project.id %}"> + Import layer + </a> + </li> + {% if CUSTOM_IMAGE %} + <li> + <a href="{% url 'newcustomimage' project.id %}"> + New custom image + </a> + </li> + {% endif %} + <li class="pull-right"> + <form class="form-inline" style="margin-bottom:0px;"> + <i class="icon-question-sign get-help heading-help" data-placement="left" title="" data-original-title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a semicolon and a task name to the recipe name, like so: <code>busybox:clean</code>"></i> + <div class="input-append"> + <input id="build-input" type="text" class="input-xlarge input-lg build-target-input" placeholder="Type the recipe you want to build" autocomplete="off" disabled> + <button id="build-button" class="btn btn-primary btn-large build-button" data-project-id="{{project.id}}" disabled>Build</button> + </div> + </form> + </li> + </ul> + </div> +{% endif %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html index b5e4192d6..c6ae2f380 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe.html @@ -2,6 +2,7 @@ {% load projecttags %} +{% block title %} {{object.name}}_{{object.version}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li><a href="{% url 'recipes' build.pk %}">Recipes</a></li> <li>{{object.name}}_{{object.version}} </li> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe_btn.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe_btn.html index 77c1b235b..baab06eb5 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe_btn.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipe_btn.html @@ -1,7 +1,15 @@ -<button data-recipe-name="{{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.pk}} build-recipe-btn" style="display:none; margin-top: 5px;" > +<button data-recipe-name="{{data.name}}" class="btn btn-block layer-exists-{{data.layer_version.pk}} build-recipe-btn" style="margin-top: 5px; + {% if data.layer_version.pk not in extra.current_layers %} + display:none; + {% endif %}" + > Build recipe </button> -<button class="btn btn-block layerbtn layer-add-{{data.layer_version.pk}}" data-layer='{ "id": {{data.layer_version.pk}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add"> +<button class="btn btn-block layerbtn layer-add-{{data.layer_version.pk}}" data-layer='{ "id": {{data.layer_version.pk}}, "name": "{{data.layer_version.layer.name}}", "layerdetailurl": "{%url 'layerdetails' extra.pid data.layer_version.pk%}"}' data-directive="add" + {% if data.layer_version.pk in extra.current_layers %} + style="display:none;" + {% endif %} +> <i class="icon-plus"></i> Add layer <i title="" class="icon-question-sign get-help" data-original-title="To build this target, you must first add the {{data.layer_version.layer.name}} layer to your project"></i> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipes.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipes.html index 5cdac437c..d14489346 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipes.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/recipes.html @@ -2,6 +2,7 @@ {% load projecttags %} +{% block title %} Recipes - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li>Recipes</li> {% endblock %} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/target.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/target.html index 65e6c4a07..4c33eaa84 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/target.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/target.html @@ -1,4 +1,5 @@ {% extends "basebuildpage.html" %} +{% block title %} Packages included - {{ target.target }} {{ target.build.machine }} - {{ target.build.project.name }} - Toaster {% endblock %} {% block localbreadcrumb %} <li>{{target.target}}</li> {% endblock localbreadcrumb%} diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html index 635098a02..ef628d9f9 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/task.html @@ -3,6 +3,7 @@ {% load projecttags %} {% load humanize %} +{% block title %} {{task.recipe.name}}_{{task.recipe.version}} {{task.task_name}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster {% endblock %} {% block localbreadcrumb %} <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> <li>{{task.recipe.name}}_{{task.recipe.version}} {{task.task_name}}</li> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html index b18b5c7c4..353410f92 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/tasks.html @@ -1,33 +1,34 @@ {% extends "basebuildpage.html" %} {% load projecttags %} +{% block title %} {{mainheading}} - {{build.target_set.all|dictsort:"target"|join:", "}} {{build.machine}} - {{build.project.name}} - Toaster{% endblock %} {% block localbreadcrumb %} -<li>{{title}}</li> +<li>{{mainheading}}</li> {% endblock %} {% block nav-tasks %} - {% if 'Tasks' == title %} + {% if 'Tasks' == mainheading %} <li class="active"><a href="{% url 'tasks' build.pk %}">Tasks</a></li> {% else %} <li><a href="{% url 'tasks' build.pk %}">Tasks</a></li> {% endif %} {% endblock %} {% block nav-buildtime %} - {% if 'Time' == title %} + {% if 'Time' == mainheading %} <li class="active"><a href="{% url 'buildtime' build.pk %}">Time</a></li> {% else %} <li><a href="{% url 'buildtime' build.pk %}">Time</a></li> {% endif %} {% endblock %} {% block nav-cpuusage %} - {% if 'CPU usage' == title %} + {% if 'CPU usage' == mainheading %} <li class="active"><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li> {% else %} <li><a href="{% url 'cpuusage' build.pk %}">CPU usage</a></li> {% endif %} {% endblock %} {% block nav-diskio %} - {% if 'Disk I/O' == title %} + {% if 'Disk I/O' == mainheading %} <li class="active"><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> {% else %} <li><a href="{% url 'diskio' build.pk %}">Disk I/O</a></li> @@ -39,7 +40,7 @@ {% if not request.GET.filter and not request.GET.search and not objects.paginator.count %} <!-- Empty - no data in database --> <div class="page-header"> - <h1>{{title}}</h1> + <h1>{{mainheading}}</h1> </div> <div class="alert alert-info lead"> No data was recorded for this build. @@ -54,7 +55,7 @@ {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %} No tasks found {%else%} - {{title}} + {{mainheading}} {%endif%} </h1> </div> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html index 9ef4c6ffe..98a715f27 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/toastertable.html @@ -12,7 +12,6 @@ tableName : "{{table_name}}", url : "{{ xhr_table_url }}?format=json", title : "{{title}}", - projectLayers : {{projectlayers|json}}, }; try { diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html b/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html index b9f8fee03..0301a6c60 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templates/unavailable_artifact.html @@ -3,6 +3,8 @@ {% load humanize %} {% load static %} +{% block title %} Build artifact no longer exists - Toaster {% endblock %} + {% block pagecontent %} <div class="row-fluid air"> diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py b/yocto-poky/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py new file mode 100644 index 000000000..04770ac6a --- /dev/null +++ b/yocto-poky/bitbake/lib/toaster/toastergui/templatetags/project_url_tag.py @@ -0,0 +1,34 @@ +from django import template +from django.core.urlresolvers import reverse + +register = template.Library() + +def project_url(parser, token): + """ + Create a URL for a project's main page; + for non-default projects, this is the configuration page; + for the default project, this is the project builds page + """ + try: + tag_name, project = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError( + "%s tag requires exactly one argument" % tag_name + ) + return ProjectUrlNode(project) + +class ProjectUrlNode(template.Node): + def __init__(self, project): + self.project = template.Variable(project) + + def render(self, context): + try: + project = self.project.resolve(context) + if project.is_default: + return reverse('projectbuilds', args=(project.id,)) + else: + return reverse('project', args=(project.id,)) + except template.VariableDoesNotExist: + return '' + +register.tag('project_url', project_url) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/tests.py b/yocto-poky/bitbake/lib/toaster/toastergui/tests.py index 4d1549b0a..9e6c46a25 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/tests.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/tests.py @@ -22,15 +22,29 @@ """Test cases for Toaster GUI and ReST.""" from django.test import TestCase +from django.test.client import RequestFactory from django.core.urlresolvers import reverse from django.utils import timezone -from orm.models import Project, Release, BitbakeVersion, ProjectTarget + +from orm.models import Project, Release, BitbakeVersion, Package, LogMessage from orm.models import ReleaseLayerSourcePriority, LayerSource, Layer, Build -from orm.models import Layer_Version, Recipe, Machine, ProjectLayer +from orm.models import Layer_Version, Recipe, Machine, ProjectLayer, Target +from orm.models import CustomImageRecipe, ProjectVariable +from orm.models import Branch + +import toastermain + +from toastergui.tables import SoftwareRecipesTable import json from bs4 import BeautifulSoup +import re PROJECT_NAME = "test project" +CLI_BUILDS_PROJECT_NAME = 'Command line builds' + +# by default, tests are run in build mode; to run in analysis mode, +# set this to False in individual test cases +toastermain.settings.BUILD_MODE = True class ViewTests(TestCase): """Tests to verify view APIs.""" @@ -39,27 +53,58 @@ class ViewTests(TestCase): bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/", branch="master", dirpath="") release = Release.objects.create(name="test release", + branch_name="master", bitbake_version=bbv) self.project = Project.objects.create_project(name=PROJECT_NAME, release=release) + now = timezone.now() + + build = Build.objects.create(project=self.project, + started_on=now, + completed_on=now) + layersrc = LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED) self.priority = ReleaseLayerSourcePriority.objects.create(release=release, layer_source=layersrc) layer = Layer.objects.create(name="base-layer", layer_source=layersrc, vcs_url="/tmp/") + branch = Branch.objects.create(name="master", layer_source=layersrc) + lver = Layer_Version.objects.create(layer=layer, project=self.project, - layer_source=layersrc, commit="master") + layer_source=layersrc, commit="master", + up_branch=branch) - Recipe.objects.create(layer_source=layersrc, name="base-recipe", - version="1.2", summary="one recipe", - description="recipe", layer_version=lver) + self.recipe1 = Recipe.objects.create(layer_source=layersrc, + name="base-recipe", + version="1.2", + summary="one recipe", + description="recipe", + layer_version=lver) Machine.objects.create(layer_version=lver, name="wisk", description="wisking machine") ProjectLayer.objects.create(project=self.project, layercommit=lver) + + self.customr = CustomImageRecipe.objects.create(\ + name="custom recipe", project=self.project, + base_recipe=self.recipe1) + + self.package = Package.objects.create(name='pkg1', recipe=self.recipe1, + build=build) + + + # recipe with project for testing AvailableRecipe table + self.recipe2 = Recipe.objects.create(layer_source=layersrc, + name="fancy-recipe", + version="1.4", + summary="a fancy recipe", + description="fancy recipe", + layer_version=lver, + file_path='/home/foo') + self.assertTrue(lver in self.project.compatible_layerversions()) def test_get_base_call_returns_html(self): @@ -181,6 +226,140 @@ class ViewTests(TestCase): data = json.loads(response.content) self.assertNotEqual(data["error"], "ok") + def test_custom_ok(self): + """Test successful return from ReST API xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': self.project.id, + 'base': self.recipe1.id} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data['error'], 'ok') + self.assertTrue('url' in data) + # get recipe from the database + recipe = CustomImageRecipe.objects.get(project=self.project, + name=params['name']) + args = (self.project.id, recipe.id,) + self.assertEqual(reverse('customrecipe', args=args), data['url']) + + def test_custom_incomplete_params(self): + """Test not passing all required parameters to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + for params in [{}, {'name': 'custom'}, + {'name': 'custom', 'project': self.project.id}]: + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_wrong_project(self): + """Test passing wrong project id to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': 0, "base": self.recipe1.id} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_wrong_base(self): + """Test passing wrong base recipe id to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': self.project.id, "base": 0} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_details(self): + """Test getting custom recipe details""" + name = "custom recipe" + url = reverse('xhr_customrecipe_id', args=(self.customr.id,)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected = {"error": "ok", + "info": {'id': self.customr.id, + 'name': name, + 'base_recipe_id': self.recipe1.id, + 'project_id': self.project.id, + } + } + self.assertEqual(json.loads(response.content), expected) + + def test_xhr_custom_del(self): + """Test deleting custom recipe""" + name = "to be deleted" + recipe = CustomImageRecipe.objects.create(\ + name=name, project=self.project, + base_recipe=self.recipe1) + url = reverse('xhr_customrecipe_id', args=(recipe.id,)) + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(response.content), {"error": "ok"}) + # try to delete not-existent recipe + url = reverse('xhr_customrecipe_id', args=(recipe.id,)) + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads(response.content)["error"], "ok") + + def test_xhr_custom_packages(self): + """Test adding and deleting package to a custom recipe""" + url = reverse('xhr_customrecipe_packages', + args=(self.customr.id, self.package.id)) + # add self.package1 to recipe + response = self.client.put(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(response.content), {"error": "ok"}) + self.assertEqual(self.customr.packages.all()[0].id, self.package.id) + # delete it + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(response.content), {"error": "ok"}) + self.assertFalse(self.customr.packages.all()) + # delete it again to test error condition + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads(response.content)["error"], "ok") + + def test_xhr_custom_packages_err(self): + """Test error conditions of xhr_customrecipe_packages""" + # test calls with wrong recipe id and wrong package id + for args in [(0, self.package.id), (self.customr.id, 0)]: + url = reverse('xhr_customrecipe_packages', args=args) + # test put and delete methods + for method in (self.client.put, self.client.delete): + response = method(url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads(response.content), + {"error": "ok"}) + + def test_software_recipes_table(self): + """Test structure returned for Software RecipesTable""" + table = SoftwareRecipesTable() + request = RequestFactory().get('/foo/', {'format': 'json'}) + response = table.get(request, pid=self.project.id) + data = json.loads(response.content) + + rows = data['rows'] + row1 = next(x for x in rows if x['name'] == self.recipe1.name) + row2 = next(x for x in rows if x['name'] == self.recipe2.name) + + self.assertEqual(response.status_code, 200, 'should be 200 OK status') + self.assertEqual(len(rows), 2, 'should be 2 recipes') + + # check other columns have been populated correctly + self.assertEqual(row1['name'], self.recipe1.name) + self.assertEqual(row1['version'], self.recipe1.version) + self.assertEqual(row1['get_description_or_summary'], + self.recipe1.description) + self.assertEqual(row1['layer_version__layer__name'], + self.recipe1.layer_version.layer.name) + self.assertEqual(row2['name'], self.recipe2.name) + self.assertEqual(row2['version'], self.recipe2.version) + self.assertEqual(row2['get_description_or_summary'], + self.recipe2.description) + self.assertEqual(row2['layer_version__layer__name'], + self.recipe2.layer_version.layer.name) + class LandingPageTests(TestCase): """ Tests for redirects on the landing page """ # disable bogus pylint message error: @@ -255,18 +434,48 @@ class LandingPageTests(TestCase): self.assertTrue('/builds' in response.url, 'should redirect to builds') -class ProjectsPageTests(TestCase): - """ Tests for projects page """ +class AllProjectsPageTests(TestCase): + """ Tests for projects page /projects/ """ - PROJECT_NAME = 'cli builds' + MACHINE_NAME = 'delorean' def setUp(self): """ Add default project manually """ - project = Project.objects.create_project(self.PROJECT_NAME, None) + project = Project.objects.create_project(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 _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 """ + bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/", + branch="master", dirpath="") + self.release = Release.objects.create(name="test release", + branch_name="master", + bitbake_version=bbv) + self.project = Project.objects.create_project(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 test_default_project_hidden(self): """ The default project should be hidden if it has no builds """ params = {"count": 10, "orderby": "updated:-", "page": 1} @@ -274,26 +483,116 @@ class ProjectsPageTests(TestCase): self.assertTrue(not('tr class="data"' in response.content), 'should be no project rows in the page') - self.assertTrue(not(self.PROJECT_NAME in response.content), + self.assertTrue(not(CLI_BUILDS_PROJECT_NAME in response.content), 'default project "cli builds" should not be in page') def test_default_project_has_build(self): """ The default project should be shown if it has builds """ - now = timezone.now() - build = Build.objects.create(project=self.default_project, - started_on=now, - completed_on=now) - build.save() + self._add_build_to_default_project() params = {"count": 10, "orderby": "updated:-", "page": 1} response = self.client.get(reverse('all-projects'), params) self.assertTrue('tr class="data"' in response.content, 'should be a project row in the page') - self.assertTrue(self.PROJECT_NAME in response.content, + self.assertTrue(CLI_BUILDS_PROJECT_NAME in response.content, 'default project "cli builds" should be in page') -class ProjectBuildsDisplayTest(TestCase): + 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() + + response = self.client.get(reverse('all-projects'), follow=True) + soup = BeautifulSoup(response.content) + + # check the release cell for the default project + attrs = {'data-project': str(self.default_project.id)} + rows = soup.find_all('tr', attrs=attrs) + self.assertEqual(len(rows), 1, 'should be one row for default project') + cells = rows[0].find_all('td', attrs={'data-project-field': 'release'}) + self.assertEqual(len(cells), 1, 'should be one release cell') + text = cells[0].select('span.muted')[0].text + self.assertEqual(text, 'Not applicable', + 'release should be not applicable for default project') + + # check the link in the release cell for the other project + attrs = {'data-project': str(self.project.id)} + rows = soup.find_all('tr', attrs=attrs) + cells = rows[0].find_all('td', attrs={'data-project-field': 'release'}) + text = cells[0].select('a')[0].text + 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() + + response = self.client.get(reverse('all-projects'), follow=True) + soup = BeautifulSoup(response.content) + + # check the machine cell for the default project + attrs = {'data-project': str(self.default_project.id)} + rows = soup.find_all('tr', attrs=attrs) + self.assertEqual(len(rows), 1, 'should be one row for default project') + cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'}) + self.assertEqual(len(cells), 1, 'should be one machine cell') + text = cells[0].select('span.muted')[0].text + self.assertEqual(text, 'Not applicable', + 'machine should be not applicable for default project') + + # check the link in the machine cell for the other project + attrs = {'data-project': str(self.project.id)} + rows = soup.find_all('tr', attrs=attrs) + cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'}) + text = cells[0].select('a')[0].text + 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, which should show machine + self._add_non_default_project() + + response = self.client.get(reverse('all-projects'), follow=True) + soup = BeautifulSoup(response.content) + + # link for default project + row = soup.find('tr', attrs={'data-project': self.default_project.id}) + cell = row.find('td', attrs={'data-project-field': 'name'}) + expected_url = reverse('projectbuilds', args=(self.default_project.id,)) + self.assertEqual(cell.find('a')['href'], expected_url, + 'link on default project name should point to builds') + + # link for other project + row = soup.find('tr', attrs={'data-project': self.project.id}) + cell = row.find('td', attrs={'data-project-field': 'name'}) + expected_url = reverse('project', args=(self.project.id,)) + self.assertEqual(cell.find('a')['href'], expected_url, + 'link on project name should point to configuration') + +class ProjectBuildsPageTests(TestCase): """ Test data at /project/X/builds is displayed correctly """ def setUp(self): @@ -303,8 +602,18 @@ class ProjectBuildsDisplayTest(TestCase): bitbake_version=bbv) self.project1 = Project.objects.create_project(name=PROJECT_NAME, release=release) + self.project1.save() + self.project2 = Project.objects.create_project(name=PROJECT_NAME, release=release) + self.project2.save() + + self.default_project = Project.objects.create_project( + name=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() @@ -338,6 +647,7 @@ class ProjectBuildsDisplayTest(TestCase): } def _get_rows_for_project(self, project_id): + """ Helper to retrieve HTML rows for a project """ url = reverse("projectbuilds", args=(project_id,)) response = self.client.get(url, follow=True) soup = BeautifulSoup(response.content) @@ -345,35 +655,273 @@ class ProjectBuildsDisplayTest(TestCase): def test_show_builds_for_project(self): """ Builds for a project should be displayed """ - build1a = Build.objects.create(**self.project1_build_success) - build1b = Build.objects.create(**self.project1_build_success) + 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_for_project_only(self): + def test_show_builds_project_only(self): """ Builds for other projects should be excluded """ - build1a = Build.objects.create(**self.project1_build_success) - build1b = Build.objects.create(**self.project1_build_success) - build1c = Build.objects.create(**self.project1_build_success) + 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 - build2a = Build.objects.create(**self.project2_build_success) - build2b = Build.objects.create(**self.project2_build_in_progress) + 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_show_builds_exclude_in_progress(self): + def test_builds_exclude_in_progress(self): """ "in progress" builds should not be shown """ - build1a = Build.objects.create(**self.project1_build_success) - build1b = Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) # shouldn't see this one - build1c = Build.objects.create(**self.project1_build_in_progress) + Build.objects.create(**self.project1_build_in_progress) # shouldn't see these two either, as they belong to a different project - build2a = Build.objects.create(**self.project2_build_success) - build2b = Build.objects.create(**self.project2_build_in_progress) + 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)
\ No newline at end of file + self.assertEqual(len(build_rows), 2) + + def test_tasks_in_projectbuilds(self): + """ Task should be shown as suffix on build name """ + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, target='bash', task='clean') + url = reverse("projectbuilds", args=(self.project1.id,)) + response = self.client.get(url, follow=True) + result = re.findall('^ +bash:clean$', response.content, re.MULTILINE) + self.assertEqual(len(result), 2) + + def test_cli_builds_hides_tabs(self): + """ + Display for command line builds should hide tabs; + note that the latest builds section is already tested in + AllBuildsPageTests, as the template is the same + """ + url = reverse("projectbuilds", args=(self.default_project.id,)) + response = self.client.get(url, follow=True) + soup = BeautifulSoup(response.content) + tabs = soup.select('#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,)) + response = self.client.get(url, follow=True) + soup = BeautifulSoup(response.content) + tabs = soup.select('#project-topbar') + self.assertEqual(len(tabs), 1, + 'should be a top bar shown for non-command-line builds') + +class AllBuildsPageTests(TestCase): + """ Tests for all builds page /builds/ """ + + def setUp(self): + bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/", + branch="master", dirpath="") + release = Release.objects.create(name="release1", + bitbake_version=bbv) + self.project1 = Project.objects.create_project(name=PROJECT_NAME, + release=release) + self.default_project = Project.objects.create_project( + name=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.default_project_build_success = { + "project": self.default_project, + "started_on": now, + "completed_on": now, + "outcome": Build.SUCCEEDED + } + + def test_show_tasks_in_allbuilds(self): + """ Task should be shown as suffix on build name """ + build = Build.objects.create(**self.project1_build_success) + Target.objects.create(build=build, target='bash', task='clean') + url = reverse('all-builds') + response = self.client.get(url, follow=True) + result = re.findall('bash:clean', response.content, re.MULTILINE) + self.assertEqual(len(result), 3) + + def test_no_run_again_for_cli_build(self): + """ "Run again" button should not be shown for command-line builds """ + build = Build.objects.create(**self.default_project_build_success) + url = reverse('all-builds') + response = self.client.get(url, follow=True) + soup = BeautifulSoup(response.content) + + attrs = {'data-latest-build-result': build.id} + result = soup.find('div', attrs=attrs) + + # shouldn't see a run again button for command-line builds + run_again_button = result.select('button') + self.assertEqual(len(run_again_button), 0) + + # should see a help icon for command-line builds + help_icon = result.select('i.get-help-green') + self.assertEqual(len(help_icon), 1) + + def test_tooltips_on_project_name(self): + """ + A tooltip should be present next to the command line + builds project name in the all builds page, but not for + other projects + """ + build1 = Build.objects.create(**self.project1_build_success) + default_build = Build.objects.create(**self.default_project_build_success) + + url = reverse('all-builds') + response = self.client.get(url, follow=True) + soup = BeautifulSoup(response.content) + + # no help icon on non-default project name + result = soup.find('tr', attrs={'data-table-build-result': build1.id}) + name = result.select('td.project-name')[0] + icons = name.select('i.get-help') + self.assertEqual(len(icons), 0, + 'should not be a help icon for non-cli builds name') + + # help icon on default project name + result = soup.find('tr', attrs={'data-table-build-result': default_build.id}) + name = result.select('td.project-name')[0] + icons = name.select('i.get-help') + self.assertEqual(len(icons), 1, + 'should be a help icon for cli builds name') + +class ProjectPageTests(TestCase): + """ Test project data at /project/X/ is displayed correctly """ + CLI_BUILDS_PROJECT_NAME = 'Command line builds' + + def test_command_line_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 in migration 0026_set_default_project.py + 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 = 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,)) + response = self.client.get(url, follow=True) + + self.assertEqual(response.status_code, 200) + +class BuildDashboardTests(TestCase): + """ Tests for the build dashboard /build/X """ + + def setUp(self): + bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/", + branch="master", dirpath="") + release = Release.objects.create(name="release1", + bitbake_version=bbv) + project = Project.objects.create_project(name=PROJECT_NAME, + release=release) + + now = timezone.now() + + self.build1 = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + # 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 + ) + + def _get_build_dashboard_errors(self): + """ + Get a list of HTML fragments representing the errors on the + build dashboard + """ + url = reverse('builddashboard', args=(self.build1.id,)) + response = self.client.get(url) + soup = BeautifulSoup(response.content) + return soup.select('#errors div.alert-error') + + def _check_for_log_message(self, log_message): + """ + Check whether the LogMessage instance <log_message> is + represented as an HTML error in the build dashboard page + """ + errors = self._get_build_dashboard_errors() + self.assertEqual(len(errors), 2) + + expected_text = log_message.message + expected_id = str(log_message.id) + + found = False + for error in errors: + error_text = error.find('pre').text + text_matches = (error_text == expected_text) + + error_id = error['data-error'] + id_matches = (error_id == expected_id) + + if text_matches and id_matches: + found = True + break + + template_vars = (expected_text, error_text, + expected_id, error_id) + assertion_error_msg = 'exception not found as error: ' \ + 'expected text "%s" and got "%s"; ' \ + 'expected ID %s and got %s' % template_vars + self.assertTrue(found, assertion_error_msg) + + def test_exceptions_show_as_errors(self): + """ + LogMessages with level EXCEPTION should display in the errors + section of the page + """ + self._check_for_log_message(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_log_message(self.critical_message) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/typeaheads.py b/yocto-poky/bitbake/lib/toaster/toastergui/typeaheads.py index d5bec58ea..dd4b7f505 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/typeaheads.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/typeaheads.py @@ -27,7 +27,7 @@ class LayersTypeAhead(ToasterTypeAhead): super(LayersTypeAhead, self).__init__() def apply_search(self, search_term, prj, request): - layers = prj.compatible_layerversions() + layers = prj.get_all_compatible_layer_versions() layers = layers.order_by('layer__name') # Unlike the other typeaheads we also don't want to show suggestions @@ -35,7 +35,8 @@ class LayersTypeAhead(ToasterTypeAhead): # layerdeps to a new layer. if ("include_added" in request.GET and request.GET['include_added'] != "true"): - layers = layers.exclude(pk__in=prj.projectlayer_equivalent_set) + layers = layers.exclude( + pk__in=prj.get_project_layer_versions(pk=True)) primary_results = layers.filter(layer__name__istartswith=search_term) secondary_results = layers.filter(layer__name__icontains=search_term).exclude(pk__in=primary_results) @@ -120,12 +121,12 @@ class RecipesTypeAhead(ToasterTypeAhead): return results class ProjectsTypeAhead(ToasterTypeAhead): - """ Typeahead for all the projects """ + """ Typeahead for all the projects, except for command line builds """ def __init__(self): super(ProjectsTypeAhead, self).__init__() def apply_search(self, search_term, prj, request): - projects = Project.objects.all().order_by("name") + projects = Project.objects.exclude(is_default=True).order_by("name") primary_results = projects.filter(name__istartswith=search_term) secondary_results = projects.filter(name__icontains=search_term).exclude(pk__in=primary_results) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/urls.py b/yocto-poky/bitbake/lib/toaster/toastergui/urls.py index 46e576144..a1adbb7be 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/urls.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/urls.py @@ -87,28 +87,29 @@ urlpatterns = patterns('toastergui.views', # the table pages that have been converted to ToasterTable widget url(r'^project/(?P<pid>\d+)/machines/$', tables.MachinesTable.as_view(template_name="generic-toastertable-page.html"), - { 'table_name': tables.MachinesTable.__name__.lower(), - 'title' : 'Compatible machines' }, name="projectmachines"), - url(r'^project/(?P<pid>\d+)/recipes/$', - tables.RecipesTable.as_view(template_name="generic-toastertable-page.html"), - { 'table_name': tables.RecipesTable.__name__.lower(), - 'title' : 'Compatible recipes' }, - name="projecttargets"), + url(r'^project/(?P<pid>\d+)/softwarerecipes/$', + tables.SoftwareRecipesTable.as_view(template_name="generic-toastertable-page.html"), + name="projectsoftwarerecipes"), + + url(r'^project/(?P<pid>\d+)/images/$', + tables.ImageRecipesTable.as_view(template_name="generic-toastertable-page.html"), name="projectimagerecipes"), + + url(r'^project/(?P<pid>\d+)/customimages/$', + tables.CustomImagesTable.as_view(template_name="generic-toastertable-page.html"), name="projectcustomimages"), + + url(r'^project/(?P<pid>\d+)/newcustomimage/$', + tables.NewCustomImagesTable.as_view(template_name="newcustomimage.html"), + name="newcustomimage"), - url(r'^project/(?P<pid>\d+)/availablerecipes/$', - tables.ProjectLayersRecipesTable.as_view(template_name="generic-toastertable-page.html"), - { 'table_name': tables.ProjectLayersRecipesTable.__name__.lower(), - 'title' : 'Recipes available for layers in the current project' }, - name="projectavailabletargets"), url(r'^project/(?P<pid>\d+)/layers/$', tables.LayersTable.as_view(template_name="generic-toastertable-page.html"), - { 'table_name': tables.LayersTable.__name__.lower(), - 'title' : 'Compatible layers' }, name="projectlayers"), + + url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$', 'layerdetails', name='layerdetails'), @@ -125,6 +126,16 @@ urlpatterns = patterns('toastergui.views', name=tables.LayerMachinesTable.__name__.lower()), + url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipeid>\d+)/selectpackages/$', + tables.SelectPackagesTable.as_view(template_name="generic-toastertable-page.html"), name="recipeselectpackages"), + + + url(r'^project/(?P<pid>\d+)/customrecipe/(?P<recipe_id>\d+)$', + 'customrecipe', + name="customrecipe"), + + + # typeahead api end points url(r'^xhr_typeahead/(?P<pid>\d+)/layers$', typeaheads.LayersTypeAhead.as_view(), name='xhr_layerstypeahead'), @@ -148,6 +159,14 @@ urlpatterns = patterns('toastergui.views', # JS Unit tests url(r'^js-unit-tests/$', 'jsunittests', name='js-unit-tests'), - # default redirection + # image customisation functionality + url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/(?P<package_id>\d+|)$', + 'xhr_customrecipe_packages', name='xhr_customrecipe_packages'), + url(r'^xhr_customrecipe/(?P<recipe_id>\d+)$', 'xhr_customrecipe_id', + name='xhr_customrecipe_id'), + url(r'^xhr_customrecipe/', 'xhr_customrecipe', + name='xhr_customrecipe'), + + # default redirection url(r'^$', RedirectView.as_view( url= 'landing')), ) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/views.py b/yocto-poky/bitbake/lib/toaster/toastergui/views.py index 8689a1251..0e255f1b8 100755 --- a/yocto-poky/bitbake/lib/toaster/toastergui/views.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/views.py @@ -26,12 +26,12 @@ import operator,re from django.db.models import F, Q, Sum, Count, Max -from django.db import IntegrityError +from django.db import IntegrityError, Error from django.shortcuts import render, redirect from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe, LogMessage, Variable from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File, Package_Dependency from orm.models import Target_Installed_Package, Target_File, Target_Image_File, BuildArtifact -from orm.models import BitbakeVersion +from orm.models import BitbakeVersion, CustomImageRecipe from bldcontrol import bbcontroller from django.views.decorators.cache import cache_control from django.core.urlresolvers import reverse, resolve @@ -45,32 +45,50 @@ from django.utils import formats from toastergui.templatetags.projecttags import json as jsonfilter import json from os.path import dirname +from functools import wraps import itertools +import mimetypes -import magic import logging logger = logging.getLogger("toaster") class MimeTypeFinder(object): - _magic = magic.Magic(flags = magic.MAGIC_MIME_TYPE) + # setting this to False enables additional non-standard mimetypes + # to be included in the guess + _strict = False - # returns the mimetype for a file path + # returns the mimetype for a file path as a string, + # or 'application/octet-stream' if the type couldn't be guessed @classmethod def get_mimetype(self, path): - return self._magic.id_filename(path) + guess = mimetypes.guess_type(path, self._strict) + guessed_type = guess[0] + if guessed_type == None: + guessed_type = 'application/octet-stream' + return guessed_type # all new sessions should come through the landing page; # determine in which mode we are running in, and redirect appropriately def landing(request): + # in build mode, we redirect to the command-line builds page + # if there are any builds for the default (cli builds) project + default_project = Project.objects.get_default_project() + default_project_builds = Build.objects.filter(project = default_project) + + if (not toastermain.settings.BUILD_MODE) and default_project_builds.count() > 0: + args = (default_project.id,) + return redirect(reverse('projectbuilds', args = args), permanent = False) + # we only redirect to projects page if there is a user-generated project + num_builds = Build.objects.all().count() user_projects = Project.objects.filter(is_default = False) has_user_project = user_projects.count() > 0 - if Build.objects.count() == 0 and has_user_project: + if num_builds == 0 and has_user_project: return redirect(reverse('all-projects'), permanent = False) - if Build.objects.all().count() > 0: + if num_builds > 0: return redirect(reverse('all-builds'), permanent = False) context = {'lvs_nos' : Layer_Version.objects.all().count()} @@ -84,9 +102,12 @@ def _get_latest_builds(prj=None): if prj is not None: queryset = queryset.filter(project = prj) + if not toastermain.settings.BUILD_MODE: + queryset = queryset.exclude(project__is_default=False) + return list(itertools.chain( - queryset.filter(outcome=Build.IN_PROGRESS).order_by("-pk"), - queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-pk")[:3] )) + queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), + queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] )) # a JSON-able dict of recent builds; for use in the Project page, xhr_ updates, and other places, as needed @@ -1215,7 +1236,7 @@ def tasks_common(request, build_id, variant, task_anchor): context = { 'objectname': variant, 'object_search_display': object_search_display, 'filter_search_display': filter_search_display, - 'title': title_variant, + 'mainheading': title_variant, 'build': build, 'objects': task_objects, 'default_orderby' : orderby, @@ -1862,11 +1883,21 @@ def image_information_dir(request, build_id, target_id, packagefile_id): return redirect(builds) # the context processor that supplies data used across all the pages - +# a context processor which runs on every request; this provides the +# projects and non_cli_projects (i.e. projects created by the user) +# variables referred to in templates, which used to determine the +# visibility of UI elements like the "New build" button def managedcontextprocessor(request): + projects = Project.objects.all() ret = { - "projects": Project.objects.all(), + "projects": projects, + "non_cli_projects": projects.exclude(is_default=True), "DEBUG" : toastermain.settings.DEBUG, + + # True if Toaster is in build mode, False otherwise + "BUILD_MODE": toastermain.settings.BUILD_MODE, + + "CUSTOM_IMAGE" : toastermain.settings.CUSTOM_IMAGE, "TOASTER_BRANCH": toastermain.settings.TOASTER_BRANCH, "TOASTER_REVISION" : toastermain.settings.TOASTER_REVISION, } @@ -1908,6 +1939,11 @@ if True: queryset = Build.objects.all() + # if in analysis mode, exclude builds for all projects except + # command line builds + if not toastermain.settings.BUILD_MODE: + queryset = queryset.exclude(project__is_default=False) + redirect_page = resolve(request.path_info).url_name context, pagesize, orderby = _build_list_helper(request, @@ -1982,7 +2018,7 @@ if True: build_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1)) # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) - build_mru = Build.objects.order_by("-started_on")[:3] + build_mru = _get_latest_builds()[:3] # calculate the exact begining of local today and yesterday, append context context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'}) @@ -2101,35 +2137,38 @@ if True: }, {'name': 'Errors', 'clclass': 'errors_no', 'qhelp': "How many errors were encountered during the build (if any)", - 'orderfield': _get_toggle_order(request, "errors_no", True), - 'ordericon':_get_toggle_order_icon(request, "errors_no"), - 'orderkey' : 'errors_no', - 'filter' : {'class' : 'errors_no', - 'label': 'Show:', - 'options' : [ - ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()), - ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()), - ] - } + # Comment out sorting and filter until YOCTO #8131 is fixed + #'orderfield': _get_toggle_order(request, "errors_no", True), + #'ordericon':_get_toggle_order_icon(request, "errors_no"), + #'orderkey' : 'errors_no', + #'filter' : {'class' : 'errors_no', + # 'label': 'Show:', + # 'options' : [ + # ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()), + # ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()), + # ] + # } }, {'name': 'Warnings', 'clclass': 'warnings_no', 'qhelp': "How many warnings were encountered during the build (if any)", - 'orderfield': _get_toggle_order(request, "warnings_no", True), - 'ordericon':_get_toggle_order_icon(request, "warnings_no"), - 'orderkey' : 'warnings_no', - 'filter' : {'class' : 'warnings_no', - 'label': 'Show:', - 'options' : [ - ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()), - ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()), - ] - } + # Comment out sorting and filter until YOCTO #8131 is fixed + #'orderfield': _get_toggle_order(request, "warnings_no", True), + #'ordericon':_get_toggle_order_icon(request, "warnings_no"), + #'orderkey' : 'warnings_no', + #'filter' : {'class' : 'warnings_no', + # 'label': 'Show:', + # 'options' : [ + # ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()), + # ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()), + # ] + # } }, {'name': 'Time', 'clclass': 'time', 'hidden' : 1, 'qhelp': "How long it took the build to finish", - 'orderfield': _get_toggle_order(request, "timespent", True), - 'ordericon':_get_toggle_order_icon(request, "timespent"), - 'orderkey' : 'timespent', + # Comment out sorting until YOCTO #8131 is fixed + #'orderfield': _get_toggle_order(request, "timespent", True), + #'ordericon':_get_toggle_order_icon(request, "timespent"), + #'orderkey' : 'timespent', }, {'name': 'Image files', 'clclass': 'output', 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory", @@ -2313,21 +2352,33 @@ if True: return context + def xhr_response(fun): + """ + Decorator for REST methods. + calls jsonfilter on the returned dictionary and returns result + as HttpResponse object of content_type application/json + """ + @wraps(fun) + def wrapper(*args, **kwds): + return HttpResponse(jsonfilter(fun(*args, **kwds)), + content_type="application/json") + return wrapper + def jsunittests(request): - """ Provides a page for the js unit tests """ - bbv = BitbakeVersion.objects.filter(branch="master").first() - release = Release.objects.filter(bitbake_version=bbv).first() + """ Provides a page for the js unit tests """ + bbv = BitbakeVersion.objects.filter(branch="master").first() + release = Release.objects.filter(bitbake_version=bbv).first() - name = "_js_unit_test_prj_" + name = "_js_unit_test_prj_" - # If there is an existing project by this name delete it. We don't want - # Lots of duplicates cluttering up the projects. - Project.objects.filter(name=name).delete() + # If there is an existing project by this name delete it. We don't want + # Lots of duplicates cluttering up the projects. + Project.objects.filter(name=name).delete() - new_project = Project.objects.create_project(name=name, release=release) + new_project = Project.objects.create_project(name=name, release=release) - context = { 'project' : new_project } - return render(request, "js-unit-tests.html", context) + context = { 'project' : new_project } + return render(request, "js-unit-tests.html", context) from django.views.decorators.csrf import csrf_exempt @csrf_exempt @@ -2582,7 +2633,155 @@ if True: return HttpResponse(jsonfilter({"error": "ok",}), content_type = "application/json") + @xhr_response + def xhr_customrecipe(request): + """ + Custom image recipe REST API + + Entry point: /xhr_customrecipe/ + Method: POST + + Args: + name: name of custom recipe to create + project: target project id of orm.models.Project + base: base recipe id of orm.models.Recipe + Returns: + {"error": "ok", + "url": <url of the created recipe>} + or + {"error": <error message>} + """ + # check if request has all required parameters + for param in ('name', 'project', 'base'): + if param not in request.POST: + return {"error": "Missing parameter '%s'" % param} + + # get project and baserecipe objects + params = {} + for name, model in [("project", Project), + ("base", Recipe)]: + value = request.POST[name] + try: + params[name] = model.objects.get(id=value) + except model.DoesNotExist: + return {"error": "Invalid %s id %s" % (name, value)} + + # create custom recipe + try: + recipe = CustomImageRecipe.objects.create( + name=request.POST["name"], + base_recipe=params["base"], + project=params["project"]) + except Error as err: + return {"error": "Can't create custom recipe: %s" % err} + + # Find the package list from the last build of this recipe/target + build = Build.objects.filter(target__target=params['base'].name, + project=params['project']).last() + + if build: + # Copy in every package + # We don't want these packages to be linked to anything because + # that underlying data may change e.g. delete a build + for package in build.package_set.all(): + # Create the duplicate + package.pk = None + package.save() + # Disassociate the package from the build + package.build = None + package.save() + recipe.packages.add(package) + else: + logger.warn("No packages found for this base recipe") + + return {"error": "ok", + "url": reverse('customrecipe', args=(params['project'].pk, + recipe.id))} + + @xhr_response + def xhr_customrecipe_id(request, recipe_id): + """ + Set of ReST API processors working with recipe id. + + Entry point: /xhr_customrecipe/<recipe_id> + + Methods: + GET - Get details of custom image recipe + DELETE - Delete custom image recipe + + Returns: + GET: + {"error": "ok", + "info": dictionary of field name -> value pairs + of the CustomImageRecipe model} + DELETE: + {"error": "ok"} + or + {"error": <error message>} + """ + objects = CustomImageRecipe.objects.filter(id=recipe_id) + if not objects: + return {"error": "Custom recipe with id=%s " + "not found" % recipe_id} + if request.method == 'GET': + values = CustomImageRecipe.objects.filter(id=recipe_id).values() + if values: + return {"error": "ok", "info": values[0]} + else: + return {"error": "Custom recipe with id=%s " + "not found" % recipe_id} + return {"error": "ok", "info": objects.values()[0]} + elif request.method == 'DELETE': + objects.delete() + return {"error": "ok"} + else: + return {"error": "Method %s is not supported" % request.method} + + @xhr_response + def xhr_customrecipe_packages(request, recipe_id, package_id): + """ + ReST API to add/remove packages to/from custom recipe. + + Entry point: /xhr_customrecipe/<recipe_id>/packages/ + + Methods: + PUT - Add package to the recipe + DELETE - Delete package from the recipe + + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + try: + recipe = CustomImageRecipe.objects.get(id=recipe_id) + except CustomImageRecipe.DoesNotExist: + return {"error": "Custom recipe with id=%s " + "not found" % recipe_id} + + if request.method == 'GET' and not package_id: + return {"error": "ok", + "packages": list(recipe.packages.values_list('id'))} + + try: + package = Package.objects.get(id=package_id) + except Package.DoesNotExist: + return {"error": "Package with id=%s " + "not found" % package_id} + + if request.method == 'PUT': + recipe.packages.add(package) + return {"error": "ok"} + elif request.method == 'DELETE': + if package in recipe.packages.all(): + recipe.packages.remove(package) + return {"error": "ok"} + else: + return {"error": "Package '%s' is not in the recipe '%s'" % \ + (package.name, recipe.name)} + else: + return {"error": "Method %s is not supported" % request.method} def importlayer(request, pid): template = "importlayer.html" @@ -2596,12 +2795,16 @@ if True: project = Project.objects.get(pk=pid) layer_version = Layer_Version.objects.get(pk=layerid) - context = { 'project' : project, - 'layerversion' : layer_version, - 'layerdeps' : { "list": [ - [{"id": y.id, "name": y.layer.name} for y in x.depends_on.get_equivalents_wpriority(project)][0] for x in layer_version.dependencies.all()]}, - 'projectlayers': map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project)) - } + context = {'project' : project, + 'layerversion' : layer_version, + 'layerdeps' : {"list": [{"id": dep.id, + "name": dep.layer.name, + "layerdetailurl": reverse('layerdetails', args=(pid, dep.pk)), + "vcs_url": dep.layer.vcs_url, + "vcs_reference": dep.get_vcs_reference()} \ + for dep in layer_version.get_alldeps(project.id)]}, + 'projectlayers': map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project)) + } return context @@ -2628,6 +2831,15 @@ if True: return(vars_managed,sorted(vars_fstypes),vars_blacklist) + def customrecipe(request, pid, recipe_id): + project = Project.objects.get(pk=pid) + context = {'project' : project, + 'projectlayers': [], + 'recipe' : CustomImageRecipe.objects.get(pk=recipe_id) + } + + return render(request, "customrecipe.html", context) + @_template_renderer("projectconf.html") def projectconf(request, pid): @@ -2733,6 +2945,9 @@ if True: context['project'] = prj _set_parameters_values(pagesize, orderby, request) + # add the most recent builds for this project + context['mru'] = _get_latest_builds(prj) + return context @@ -2797,7 +3012,7 @@ if True: if file_name is None: raise Exception("Could not handle artifact %s id %s" % (artifact_type, artifact_id)) else: - content_type = b.buildrequest.environment.get_artifact_type(file_name) + content_type = MimeTypeFinder.get_mimetype(file_name) fsock = b.buildrequest.environment.get_artifact(file_name) file_name = os.path.basename(file_name) # we assume that the build environment system has the same path conventions as host @@ -2833,6 +3048,10 @@ if True: queryset_all = queryset_all.filter(Q(is_default=False) | q_default_with_builds) + # if in BUILD_MODE, exclude everything but the command line builds project + if not toastermain.settings.BUILD_MODE: + queryset_all = queryset_all.exclude(is_default=False) + # boilerplate code that takes a request for an object type and returns a queryset # for that object type. copypasta for all needed table searches (filter_string, search_term, ordering_string) = _search_tuple(request, Project) diff --git a/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py b/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py index eb2914d87..6bb388936 100644 --- a/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py +++ b/yocto-poky/bitbake/lib/toaster/toastergui/widgets.py @@ -20,6 +20,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from django.views.generic import View, TemplateView +from django.views.decorators.cache import cache_control from django.shortcuts import HttpResponse from django.http import HttpResponseBadRequest from django.core import serializers @@ -38,6 +39,9 @@ import collections import operator import re +import logging +logger = logging.getLogger("toaster") + from toastergui.views import objtojson class ToasterTable(TemplateView): @@ -45,7 +49,7 @@ class ToasterTable(TemplateView): super(ToasterTable, self).__init__() if 'template_name' in kwargs: self.template_name = kwargs['template_name'] - self.title = None + self.title = "Table" self.queryset = None self.columns = [] self.filters = {} @@ -61,6 +65,18 @@ class ToasterTable(TemplateView): orderable=True, field_name="id") + # prevent HTTP caching of table data + @cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True) + def dispatch(self, *args, **kwargs): + return super(ToasterTable, self).dispatch(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(ToasterTable, self).get_context_data(**kwargs) + context['title'] = self.title + context['table_name'] = type(self).__name__.lower() + + return context + def get(self, request, *args, **kwargs): if request.GET.get('format', None) == 'json': @@ -219,7 +235,8 @@ class ToasterTable(TemplateView): """Creates a query based on the model's search_allowed_fields""" if not hasattr(self.queryset.model, 'search_allowed_fields'): - raise Exception("Err Search fields aren't defined in the model") + raise Exception("Search fields aren't defined in the model %s" + % self.queryset.model) search_queries = [] for st in search_term.split(" "): @@ -242,11 +259,14 @@ class ToasterTable(TemplateView): search = request.GET.get("search", None) filters = request.GET.get("filter", None) orderby = request.GET.get("orderby", None) + nocache = request.GET.get("nocache", None) # Make a unique cache name cache_name = self.__class__.__name__ for key, val in request.GET.iteritems(): + if key == 'nocache': + continue cache_name = cache_name + str(key) + str(val) for key, val in kwargs.iteritems(): @@ -254,9 +274,14 @@ class ToasterTable(TemplateView): # No special chars allowed in the cache name apart from dash cache_name = re.sub(r'[^A-Za-z0-9-]', "", cache_name) + + if nocache: + cache.delete(cache_name) + data = cache.get(cache_name) if data: + logger.debug("Got cache data for table '%s'" % self.title) return data self.setup_columns(**kwargs) @@ -330,33 +355,6 @@ class ToasterTable(TemplateView): return data -class ToasterTemplateView(TemplateView): - # renders a instance in a template, or returns the context as json - # the class-equivalent of the _template_renderer decorator for views - - def __init__(self, *args, **kwargs): - super(ToasterTemplateView, self).__init__(*args, **kwargs) - self.context_entries = [] - - def get(self, *args, **kwargs): - if self.request.GET.get('format', None) == 'json': - from django.core.urlresolvers import reverse - from django.shortcuts import HttpResponse - from views import objtojson - from toastergui.templatetags.projecttags import json as jsonfilter - - context = self.get_context_data(**kwargs) - - for x in context.keys(): - if x not in self.context_entries: - del context[x] - - context["error"] = "ok" - - return HttpResponse(jsonfilter(context, default=objtojson ), - content_type = "application/json; charset=utf-8") - - return super(ToasterTemplateView, self).get(*args, **kwargs) class ToasterTypeAhead(View): """ A typeahead mechanism to support the front end typeahead widgets """ diff --git a/yocto-poky/bitbake/lib/toaster/toastermain/settings.py b/yocto-poky/bitbake/lib/toaster/toastermain/settings.py index b149a5ed8..b28ddb2b0 100644 --- a/yocto-poky/bitbake/lib/toaster/toastermain/settings.py +++ b/yocto-poky/bitbake/lib/toaster/toastermain/settings.py @@ -23,6 +23,11 @@ import os, re +# Temporary toggle for Image customisation +CUSTOM_IMAGE = False +if os.environ.get("CUSTOM_IMAGE", None) is not None: + CUSTOM_IMAGE = True + DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -87,11 +92,9 @@ if 'DATABASE_URL' in os.environ: else: raise Exception("FIXME: Please implement missing database url schema for url: %s" % dburl) - +BUILD_MODE = False if 'TOASTER_MANAGED' in os.environ and os.environ['TOASTER_MANAGED'] == "1": - MANAGED = True -else: - MANAGED = False + BUILD_MODE = True # Allows current database settings to be exported as a DATABASE_URL environment variable value diff --git a/yocto-poky/bitbake/lib/toaster/toastermain/urls.py b/yocto-poky/bitbake/lib/toaster/toastermain/urls.py index 521588a6a..6c4a953b4 100644 --- a/yocto-poky/bitbake/lib/toaster/toastermain/urls.py +++ b/yocto-poky/bitbake/lib/toaster/toastermain/urls.py @@ -60,7 +60,7 @@ if toastermain.settings.DEBUG_PANEL_ENABLED: #logger.info("Enabled django_toolbar extension") -if toastermain.settings.MANAGED: +if toastermain.settings.BUILD_MODE: urlpatterns = [ # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), |