Note: This change is to support a project that I am working on. You should see no change in the behavior of your current Autotest installations.
-----
Implement the models and set up the RPC framework for the Test Planner
Signed-off-by: James Ren <jamesren@google.com>
git-svn-id: http://test.kernel.org/svn/autotest/trunk@4039 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/frontend_test_utils.py b/frontend/afe/frontend_test_utils.py
index 51b9bcb..efd6100 100644
--- a/frontend/afe/frontend_test_utils.py
+++ b/frontend/afe/frontend_test_utils.py
@@ -89,11 +89,12 @@
thread_local.set_user(self.user)
- def _frontend_common_setup(self):
+ def _frontend_common_setup(self, fill_data=True):
self.god = mock.mock_god()
self._open_test_db()
self._setup_dummy_user()
- self._fill_in_test_data()
+ if fill_data:
+ self._fill_in_test_data()
def _frontend_common_teardown(self):
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index d370bf2..a798416 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -943,3 +943,50 @@
self.delete_attribute(attribute)
else:
self.set_attribute(attribute, value)
+
+
+class ModelWithHashManager(dbmodels.Manager):
+ """Manager for use with the ModelWithHash abstract model class"""
+
+ def create(self, **kwargs):
+ raise Exception('ModelWithHash manager should use get_or_create() '
+ 'instead of create()')
+
+
+ def get_or_create(self, **kwargs):
+ kwargs['the_hash'] = self.model._compute_hash(**kwargs)
+ return super(ModelWithHashManager, self).get_or_create(**kwargs)
+
+
+class ModelWithHash(dbmodels.Model):
+ """Superclass with methods for dealing with a hash column"""
+
+ the_hash = dbmodels.CharField(max_length=40, unique=True)
+
+ objects = ModelWithHashManager()
+
+ class Meta:
+ abstract = True
+
+
+ @classmethod
+ def _compute_hash(cls, **kwargs):
+ raise NotImplementedError('Subclasses must override _compute_hash()')
+
+
+ def save(self, force_insert=False, **kwargs):
+ """Prevents saving the model in most cases
+
+ We want these models to be immutable, so the generic save() operation
+ will not work. These models should be instantiated through their the
+ model.objects.get_or_create() method instead.
+
+ The exception is that save(force_insert=True) will be allowed, since
+ that creates a new row. However, the preferred way to make instances of
+ these models is through the get_or_create() method.
+ """
+ if not force_insert:
+ # Allow a forced insert to happen; if it's a duplicate, the unique
+ # constraint will catch it later anyways
+ raise Exception('ModelWithHash is immutable')
+ super(ModelWithHash, self).save(force_insert=force_insert, **kwargs)
diff --git a/frontend/afe/rpc_utils.py b/frontend/afe/rpc_utils.py
index eb86cf4..26555d9 100644
--- a/frontend/afe/rpc_utils.py
+++ b/frontend/afe/rpc_utils.py
@@ -5,7 +5,7 @@
__author__ = 'showard@google.com (Steve Howard)'
-import datetime, os
+import datetime, os, sys
import django.http
from autotest_lib.frontend.afe import models, model_logic
@@ -620,3 +620,19 @@
special_task_index += 1
return interleaved_entries
+
+
+def get_sha1_hash(source):
+ """Gets the SHA-1 hash of the source string
+
+ @param source The string to hash
+ """
+ if sys.version_info < (2,5):
+ import sha
+ digest = sha.new()
+ else:
+ import hashlib
+ digest = hashlib.sha1()
+
+ digest.update(source)
+ return digest.hexdigest()
diff --git a/frontend/afe/urls.py b/frontend/afe/urls.py
index 78cd5fb..976ff8b 100644
--- a/frontend/afe/urls.py
+++ b/frontend/afe/urls.py
@@ -1,34 +1,19 @@
from django.conf.urls.defaults import *
-import os
-from autotest_lib.frontend import settings
+import common
+from autotest_lib.frontend import settings, urls_common
from autotest_lib.frontend.afe.feeds import feed
feeds = {
'jobs' : feed.JobFeed
}
-pattern_list = [
- (r'^(?:|noauth/)rpc/', 'frontend.afe.views.handle_rpc'),
- (r'^rpc_doc', 'frontend.afe.views.rpc_documentation'),
- ]
+pattern_list, debug_pattern_list = (
+ urls_common.generate_pattern_lists('frontend.afe', 'AfeClient'))
-debug_pattern_list = [
- (r'^model_doc/', 'frontend.afe.views.model_documentation'),
- # for GWT hosted mode
- (r'^(?P<forward_addr>autotest.*)', 'frontend.afe.views.gwt_forward'),
- # for GWT compiled files
- (r'^client/(?P<path>.*)$', 'django.views.static.serve',
- {'document_root': os.path.join(os.path.dirname(__file__), '..', 'client',
- 'www')}),
- # redirect / to compiled client
- (r'^$', 'django.views.generic.simple.redirect_to',
- {'url': 'client/autotest.AfeClient/AfeClient.html'}),
-
- # Job feeds
- (r'^feeds/(?P<url>.*)/$', 'frontend.afe.feeds.feed.feed_view',
- {'feed_dict': feeds})
-
-]
+# Job feeds
+debug_pattern_list.append((
+ r'^feeds/(?P<url>.*)/$', 'frontend.afe.feeds.feed.feed_view',
+ {'feed_dict': feeds}))
if settings.DEBUG:
pattern_list += debug_pattern_list
diff --git a/frontend/afe/views.py b/frontend/afe/views.py
index 32beb7a..6c938d9 100644
--- a/frontend/afe/views.py
+++ b/frontend/afe/views.py
@@ -4,6 +4,7 @@
from django.http import HttpResponseServerError
from django.template import Context, loader
from autotest_lib.client.common_lib import utils
+from autotest_lib.frontend import views_common
from autotest_lib.frontend.afe import models, rpc_handler, rpc_interface
from autotest_lib.frontend.afe import rpc_utils
@@ -26,13 +27,9 @@
def model_documentation(request):
- doc = '<h2>Models</h2>\n'
- for model_name in ('Label', 'Host', 'Test', 'User', 'AclGroup', 'Job',
- 'AtomicGroup'):
- model_class = getattr(models, model_name)
- doc += '<h3>%s</h3>\n' % model_name
- doc += '<pre>\n%s</pre>\n' % model_class.__doc__
- return HttpResponse(doc)
+ model_names = ('Label', 'Host', 'Test', 'User', 'AclGroup', 'Job',
+ 'AtomicGroup')
+ return views_common.model_documentation(models, model_names)
def redirect_with_extra_data(request, url, **kwargs):
diff --git a/frontend/migrations/045_test_planner_framework.py b/frontend/migrations/045_test_planner_framework.py
new file mode 100644
index 0000000..df5e0ec
--- /dev/null
+++ b/frontend/migrations/045_test_planner_framework.py
@@ -0,0 +1,242 @@
+UP_SQL = """\
+BEGIN;
+
+SET storage_engine = InnoDB;
+
+CREATE TABLE `planner_plans` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `name` varchar(255) NOT NULL UNIQUE,
+ `label_override` varchar(255) NULL,
+ `support` longtext NOT NULL,
+ `complete` bool NOT NULL,
+ `dirty` bool NOT NULL,
+ `initialized` bool NOT NULL
+)
+;
+
+
+CREATE TABLE `planner_hosts` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `host_id` integer NOT NULL,
+ `complete` bool NOT NULL,
+ `blocked` bool NOT NULL
+)
+;
+ALTER TABLE `planner_hosts` ADD CONSTRAINT hosts_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_hosts` ADD CONSTRAINT hosts_host_id_fk FOREIGN KEY (`host_id`) REFERENCES `afe_hosts` (`id`);
+
+
+CREATE TABLE `planner_test_control_files` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `the_hash` varchar(40) NOT NULL UNIQUE,
+ `contents` longtext NOT NULL
+)
+;
+
+
+CREATE TABLE `planner_tests` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `control_file_id` integer NOT NULL,
+ `execution_order` integer NOT NULL
+)
+;
+ALTER TABLE `planner_tests` ADD CONSTRAINT tests_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_tests` ADD CONSTRAINT tests_control_file_id_fk FOREIGN KEY (`control_file_id`) REFERENCES `planner_test_control_files` (`id`);
+
+
+CREATE TABLE `planner_test_jobs` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `test_id` integer NOT NULL,
+ `afe_job_id` integer NOT NULL
+)
+;
+ALTER TABLE `planner_test_jobs` ADD CONSTRAINT test_jobs_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_test_jobs` ADD CONSTRAINT test_jobs_test_id_fk FOREIGN KEY (`test_id`) REFERENCES `planner_tests` (`id`);
+ALTER TABLE `planner_test_jobs` ADD CONSTRAINT test_jobs_afe_job_id_fk FOREIGN KEY (`afe_job_id`) REFERENCES `afe_jobs` (`id`);
+CREATE TABLE `planner_bugs` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `external_uid` varchar(255) NOT NULL UNIQUE
+)
+;
+
+
+CREATE TABLE `planner_test_runs` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `test_job_id` integer NOT NULL,
+ `tko_test_id` integer(10) UNSIGNED NOT NULL,
+ `status` varchar(16) NOT NULL,
+ `finalized` bool NOT NULL,
+ `seen` bool NOT NULL,
+ `triaged` bool NOT NULL
+)
+;
+ALTER TABLE `planner_test_runs` ADD CONSTRAINT test_runs_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_test_runs` ADD CONSTRAINT test_runs_test_job_id_fk FOREIGN KEY (`test_job_id`) REFERENCES `planner_test_jobs` (`id`);
+ALTER TABLE `planner_test_runs` ADD CONSTRAINT test_runs_tko_test_id_fk FOREIGN KEY (`tko_test_id`) REFERENCES `tko.tko_tests` (`test_idx`);
+
+
+CREATE TABLE `planner_data_types` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `name` varchar(255) NOT NULL,
+ `db_table` varchar(255) NOT NULL
+)
+;
+
+
+CREATE TABLE `planner_history` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `action_id` integer NOT NULL,
+ `user_id` integer NOT NULL,
+ `data_type_id` integer NOT NULL,
+ `object_id` integer NOT NULL,
+ `old_object_repr` longtext NOT NULL,
+ `new_object_repr` longtext NOT NULL,
+ `time` datetime NOT NULL
+)
+;
+ALTER TABLE `planner_history` ADD CONSTRAINT history_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_history` ADD CONSTRAINT history_user_id_fk FOREIGN KEY (`user_id`) REFERENCES `afe_users` (`id`);
+ALTER TABLE `planner_history` ADD CONSTRAINT history_data_type_id_fk FOREIGN KEY (`data_type_id`) REFERENCES `planner_data_types` (`id`);
+
+
+CREATE TABLE `planner_saved_objects` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `user_id` integer NOT NULL,
+ `type` varchar(16) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `encoded_object` longtext NOT NULL,
+ UNIQUE (`user_id`, `type`, `name`)
+)
+;
+ALTER TABLE `planner_saved_objects` ADD CONSTRAINT saved_objects_user_id_fk FOREIGN KEY (`user_id`) REFERENCES `afe_users` (`id`);
+
+
+CREATE TABLE `planner_custom_queries` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `query` longtext NOT NULL
+)
+;
+ALTER TABLE `planner_custom_queries` ADD CONSTRAINT custom_queries_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+
+
+CREATE TABLE `planner_keyvals` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `the_hash` varchar(40) NOT NULL UNIQUE,
+ `key` varchar(1024) NOT NULL,
+ `value` varchar(1024) NOT NULL
+)
+;
+
+
+CREATE TABLE `planner_autoprocess` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `condition` longtext NOT NULL,
+ `enabled` bool NOT NULL,
+ `reason_override` varchar(255) NULL
+)
+;
+ALTER TABLE `planner_autoprocess` ADD CONSTRAINT autoprocess_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+
+
+CREATE TABLE `planner_plan_owners` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `plan_id` integer NOT NULL,
+ `user_id` integer NOT NULL,
+ UNIQUE (`plan_id`, `user_id`)
+)
+;
+ALTER TABLE `planner_plan_owners` ADD CONSTRAINT plan_owners_plan_id_fk FOREIGN KEY (`plan_id`) REFERENCES `planner_plans` (`id`);
+ALTER TABLE `planner_plan_owners` ADD CONSTRAINT plan_owners_user_id_fk FOREIGN KEY (`user_id`) REFERENCES `afe_users` (`id`);
+
+
+CREATE TABLE `planner_test_run_bugs` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `testrun_id` integer NOT NULL,
+ `bug_id` integer NOT NULL,
+ UNIQUE (`testrun_id`, `bug_id`)
+)
+;
+ALTER TABLE `planner_test_run_bugs` ADD CONSTRAINT test_run_bugs_testrun_id_fk FOREIGN KEY (`testrun_id`) REFERENCES `planner_test_runs` (`id`);
+ALTER TABLE `planner_test_run_bugs` ADD CONSTRAINT test_run_bugs_bug_id_fk FOREIGN KEY (`bug_id`) REFERENCES `planner_bugs` (`id`);
+
+
+CREATE TABLE `planner_autoprocess_labels` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `autoprocess_id` integer NOT NULL,
+ `testlabel_id` integer NOT NULL,
+ UNIQUE (`autoprocess_id`, `testlabel_id`)
+)
+;
+ALTER TABLE `planner_autoprocess_labels` ADD CONSTRAINT autoprocess_labels_autoprocess_id_fk FOREIGN KEY (`autoprocess_id`) REFERENCES `planner_autoprocess` (`id`);
+ALTER TABLE `planner_autoprocess_labels` ADD CONSTRAINT autoprocess_labels_testlabel_id_fk FOREIGN KEY (`testlabel_id`) REFERENCES `tko.tko_test_labels` (`id`);
+
+
+CREATE TABLE `planner_autoprocess_keyvals` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `autoprocess_id` integer NOT NULL,
+ `keyval_id` integer NOT NULL,
+ UNIQUE (`autoprocess_id`, `keyval_id`)
+)
+;
+ALTER TABLE `planner_autoprocess_keyvals` ADD CONSTRAINT autoprocess_keyvals_autoprocess_id_fk FOREIGN KEY (`autoprocess_id`) REFERENCES `planner_autoprocess` (`id`);
+ALTER TABLE `planner_autoprocess_keyvals` ADD CONSTRAINT autoprocess_keyvals_keyval_id_fk FOREIGN KEY (`keyval_id`) REFERENCES `planner_keyvals` (`id`);
+
+
+CREATE TABLE `planner_autoprocess_bugs` (
+ `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ `autoprocess_id` integer NOT NULL,
+ `bug_id` integer NOT NULL,
+ UNIQUE (`autoprocess_id`, `bug_id`)
+)
+;
+ALTER TABLE `planner_autoprocess_bugs` ADD CONSTRAINT autoprocess_bugs_autoprocess_id_fk FOREIGN KEY (`autoprocess_id`) REFERENCES `planner_autoprocess` (`id`);
+ALTER TABLE `planner_autoprocess_bugs` ADD CONSTRAINT autoprocess_bugs_bug_id_fk FOREIGN KEY (`bug_id`) REFERENCES `planner_bugs` (`id`);
+
+
+CREATE INDEX `planner_hosts_plan_id` ON `planner_hosts` (`plan_id`);
+CREATE INDEX `planner_hosts_host_id` ON `planner_hosts` (`host_id`);
+CREATE INDEX `planner_tests_plan_id` ON `planner_tests` (`plan_id`);
+CREATE INDEX `planner_tests_control_file_id` ON `planner_tests` (`control_file_id`);
+CREATE INDEX `planner_test_jobs_plan_id` ON `planner_test_jobs` (`plan_id`);
+CREATE INDEX `planner_test_jobs_test_id` ON `planner_test_jobs` (`test_id`);
+CREATE INDEX `planner_test_jobs_afe_job_id` ON `planner_test_jobs` (`afe_job_id`);
+CREATE INDEX `planner_test_runs_plan_id` ON `planner_test_runs` (`plan_id`);
+CREATE INDEX `planner_test_runs_test_job_id` ON `planner_test_runs` (`test_job_id`);
+CREATE INDEX `planner_test_runs_tko_test_id` ON `planner_test_runs` (`tko_test_id`);
+CREATE INDEX `planner_history_plan_id` ON `planner_history` (`plan_id`);
+CREATE INDEX `planner_history_user_id` ON `planner_history` (`user_id`);
+CREATE INDEX `planner_history_data_type_id` ON `planner_history` (`data_type_id`);
+CREATE INDEX `planner_saved_objects_user_id` ON `planner_saved_objects` (`user_id`);
+CREATE INDEX `planner_custom_queries_plan_id` ON `planner_custom_queries` (`plan_id`);
+CREATE INDEX `planner_autoprocess_plan_id` ON `planner_autoprocess` (`plan_id`);
+
+COMMIT;
+"""
+
+DOWN_SQL = """\
+DROP TABLE IF EXISTS planner_autoprocess_labels;
+DROP TABLE IF EXISTS planner_autoprocess_bugs;
+DROP TABLE IF EXISTS planner_autoprocess_keyvals;
+DROP TABLE IF EXISTS planner_autoprocess;
+DROP TABLE IF EXISTS planner_custom_queries;
+DROP TABLE IF EXISTS planner_saved_objects;
+DROP TABLE IF EXISTS planner_history;
+DROP TABLE IF EXISTS planner_data_types;
+DROP TABLE IF EXISTS planner_hosts;
+DROP TABLE IF EXISTS planner_keyvals;
+DROP TABLE IF EXISTS planner_plan_owners;
+DROP TABLE IF EXISTS planner_test_run_bugs;
+DROP TABLE IF EXISTS planner_test_runs;
+DROP TABLE IF EXISTS planner_test_jobs;
+DROP TABLE IF EXISTS planner_tests;
+DROP TABLE IF EXISTS planner_test_control_files;
+DROP TABLE IF EXISTS planner_bugs;
+DROP TABLE IF EXISTS planner_plans;
+"""
diff --git a/frontend/planner/common.py b/frontend/planner/common.py
new file mode 100644
index 0000000..1edf302
--- /dev/null
+++ b/frontend/planner/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, '..', '..'))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/frontend/planner/models.py b/frontend/planner/models.py
new file mode 100644
index 0000000..a7e8af7
--- /dev/null
+++ b/frontend/planner/models.py
@@ -0,0 +1,363 @@
+from django.db import models as dbmodels
+import common
+from autotest_lib.frontend.afe import models as afe_models
+from autotest_lib.frontend.afe import model_logic, rpc_utils
+from autotest_lib.new_tko.tko import models as tko_models
+from autotest_lib.client.common_lib import enum
+
+
+class Plan(dbmodels.Model):
+ """A test plan
+
+ Required:
+ name: Plan name, unique
+ complete: True if the plan is completed
+ dirty: True if the plan has been changed since the execution engine has
+ last seen it
+ initialized: True if the plan has started
+
+ Optional:
+ label_override: A label to apply to each Autotest job.
+ support: The global support object to apply to this plan
+ """
+ name = dbmodels.CharField(max_length=255, unique=True)
+ label_override = dbmodels.CharField(max_length=255, null=True, blank=True)
+ support = dbmodels.TextField(blank=True)
+ complete = dbmodels.BooleanField(default=False)
+ dirty = dbmodels.BooleanField(default=False)
+ initialized = dbmodels.BooleanField(default=False)
+
+ owners = dbmodels.ManyToManyField(afe_models.User,
+ db_table='planner_plan_owners')
+ hosts = dbmodels.ManyToManyField(afe_models.Host, through='Host')
+
+ class Meta:
+ db_table = 'planner_plans'
+
+
+ def __unicode__(self):
+ return unicode(self.name)
+
+
+class ModelWithPlan(dbmodels.Model):
+ """Superclass for models that have a plan_id
+
+ Required:
+ plan: The associated test plan
+ """
+ plan = dbmodels.ForeignKey(Plan)
+
+ class Meta:
+ abstract = True
+
+
+ def __unicode__(self):
+ return u'%s (%s)' % (self._get_details_unicode(), self.plan.name)
+
+
+ def _get_details_unicode(self):
+ """Gets the first part of the unicode string
+
+ subclasses must override this method
+ """
+ raise NotImplementedError(
+ 'Subclasses must override _get_details_unicode()')
+
+
+class Host(ModelWithPlan):
+ """A plan host
+
+ Required:
+ host: The AFE host
+ complete: True if and only if this host is finished in the test plan
+ blocked: True if and only if the host is blocked (not executing tests)
+ """
+ host = dbmodels.ForeignKey(afe_models.Host)
+ complete = dbmodels.BooleanField(default=False)
+ blocked = dbmodels.BooleanField(default=False)
+
+ class Meta:
+ db_table = 'planner_hosts'
+
+
+ def _get_details_unicode(self):
+ return 'Host: %s' % host.hostname
+
+
+class ControlFile(model_logic.ModelWithHash):
+ """A control file. Immutable once added to the table
+
+ Required:
+ contents: The text of the control file
+
+ Others:
+ control_hash: The SHA1 hash of the control file, for duplicate detection
+ and fast search
+ """
+ contents = dbmodels.TextField()
+
+ class Meta:
+ db_table = 'planner_test_control_files'
+
+
+ @classmethod
+ def _compute_hash(cls, **kwargs):
+ return rpc_utils.get_sha1_hash(kwargs['contents'])
+
+
+ def __unicode__(self):
+ return u'Control file id %s (SHA1: %s)' % (self.id, self.control_hash)
+
+
+class Test(ModelWithPlan):
+ """A planned test
+
+ Required:
+ test_control_file: The control file to run
+ execution_order: An integer describing when this test should be run in
+ the test plan
+ """
+ control_file = dbmodels.ForeignKey(ControlFile)
+ execution_order = dbmodels.IntegerField(blank=True)
+
+ class Meta:
+ db_table = 'planner_tests'
+ ordering = ('execution_order',)
+
+
+ def _get_details_unicode(self):
+ return 'Planned test - Control file id %s' % test_control_file.id
+
+
+class Job(ModelWithPlan):
+ """Represents an Autotest job initiated for a test plan
+
+ Required:
+ test: The Test associated with this Job
+ afe_job: The Autotest job
+ """
+ test = dbmodels.ForeignKey(Test)
+ afe_job = dbmodels.ForeignKey(afe_models.Job)
+
+ class Meta:
+ db_table = 'planner_test_jobs'
+
+
+ def _get_details_unicode(self):
+ return 'AFE job %s' % afe_job.id
+
+
+class Bug(dbmodels.Model):
+ """Represents a bug ID
+
+ Required:
+ external_uid: External unique ID for the bug
+ """
+ external_uid = dbmodels.CharField(max_length=255, unique=True)
+
+ class Meta:
+ db_table = 'planner_bugs'
+
+
+ def __unicode__(self):
+ return u'Bug external ID %s' % self.external_uid
+
+
+class TestRun(ModelWithPlan):
+ """An individual test run from an Autotest job for the test plan.
+
+ Each Job object may have multiple TestRun objects associated with it.
+
+ Required:
+ test_job: The Job object associated with this TestRun
+ tko_test: The TKO Test associated with this TestRun
+ status: One of 'Active', 'Passed', 'Failed'
+ finalized: True if and only if the TestRun is ready to be shown in
+ triage
+ invalidated: True if and only if a user has decided to invalidate this
+ TestRun's results
+ seen: True if and only if a user has marked this TestRun as "seen"
+ triaged: True if and only if the TestRun no longer requires any user
+ intervention
+
+ Optional:
+ bugs: Bugs filed that a relevant to this run
+ """
+ Status = enum.Enum('Active', 'Passed', 'Failed', string_values=True)
+
+ test_job = dbmodels.ForeignKey(Job)
+ tko_test = dbmodels.ForeignKey(tko_models.Test)
+ status = dbmodels.CharField(max_length=16, choices=Status.choices())
+ finalized = dbmodels.BooleanField(default=False)
+ seen = dbmodels.BooleanField(default=False)
+ triaged = dbmodels.BooleanField(default=False)
+
+ bugs = dbmodels.ManyToManyField(Bug, null=True,
+ db_table='planner_test_run_bugs')
+
+ class Meta:
+ db_table = 'planner_test_runs'
+
+
+ def _get_details_unicode(self):
+ return 'Test Run: %s' % self.id
+
+
+class DataType(dbmodels.Model):
+ """Encodes the data model types
+
+ For use in the history table, to identify the type of object that was
+ changed.
+
+ Required:
+ name: The name of the data type
+ db_table: The name of the database table that stores this type
+ """
+ name = dbmodels.CharField(max_length=255)
+ db_table = dbmodels.CharField(max_length=255)
+
+ class Meta:
+ db_table = 'planner_data_types'
+
+
+ def __unicode__(self):
+ return u'Data type %s (stored in table %s)' % (self.name, self.db_table)
+
+
+class History(ModelWithPlan):
+ """Represents a history action
+
+ Required:
+ action_id: An arbitrary ID that uniquely identifies the user action
+ related to the history entry. One user action may result in
+ multiple history entries
+ user: The user who initiated the change
+ data_type: The type of object that was changed
+ object_id: Value of the primary key field for the changed object
+ old_object_repr: A string representation of the object before the change
+ new_object_repr: A string representation of the object after the change
+
+ Others:
+ time: A timestamp. Automatically generated.
+ """
+ action_id = dbmodels.IntegerField()
+ user = dbmodels.ForeignKey(afe_models.User)
+ data_type = dbmodels.ForeignKey(DataType)
+ object_id = dbmodels.IntegerField()
+ old_object_repr = dbmodels.TextField(blank=True)
+ new_object_repr = dbmodels.TextField(blank=True)
+
+ time = dbmodels.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ db_table = 'planner_history'
+
+
+ def _get_details_unicode(self):
+ return 'History entry: %s => %s' % (self.old_object_repr,
+ self.new_object_repr)
+
+
+class SavedObject(dbmodels.Model):
+ """A saved object that can be recalled at certain points in the UI
+
+ Required:
+ user: The creator of the object
+ object_type: One of 'support', 'triage', 'autoprocess', 'custom_query'
+ name: The name given to the object
+ encoded_object: The actual object
+ """
+ Type = enum.Enum('support', 'triage', 'autoprocess', 'custom_query',
+ string_values=True)
+
+ user = dbmodels.ForeignKey(afe_models.User)
+ object_type = dbmodels.CharField(max_length=16,
+ choices=Type.choices(), db_column='type')
+ name = dbmodels.CharField(max_length=255)
+ encoded_object = dbmodels.TextField()
+
+ class Meta:
+ db_table = 'planner_saved_objects'
+ unique_together = ('user', 'object_type', 'name')
+
+
+ def __unicode__(self):
+ return u'Saved %s object: %s, by %s' % (self.object_type, self.name,
+ self.user.login)
+
+
+class CustomQuery(ModelWithPlan):
+ """A custom SQL query for the triage page
+
+ Required:
+ query: the SQL WHERE clause to attach to the main query
+ """
+ query = dbmodels.TextField()
+
+ class Meta:
+ db_table = 'planner_custom_queries'
+
+
+ def _get_details_unicode(self):
+ return 'Custom Query: %s' % self.query
+
+
+class KeyVal(model_logic.ModelWithHash):
+ """Represents a keyval. Immutable once added to the table.
+
+ Required:
+ key: The key
+ value: The value
+
+ Others:
+ keyval_hash: The result of SHA1(SHA1(key) ++ value), for duplicate
+ detection and fast search.
+ """
+ key = dbmodels.CharField(max_length=1024)
+ value = dbmodels.CharField(max_length=1024)
+
+ class Meta:
+ db_table = 'planner_keyvals'
+
+
+ @classmethod
+ def _compute_hash(cls, **kwargs):
+ round1 = rpc_utils.get_sha1_hash(kwargs['key'])
+ return rpc_utils.get_sha1_hash(round1 + kwargs['value'])
+
+
+ def __unicode__(self):
+ return u'Keyval: %s = %s' % (self.key, self.value)
+
+
+class AutoProcess(ModelWithPlan):
+ """An autoprocessing directive to perform on test runs that enter triage
+
+ Required:
+ condition: A SQL WHERE clause. The autoprocessing will be applied if the
+ test run matches this condition
+ enabled: If this is False, this autoprocessing entry will not be applied
+
+ Optional:
+ labels: Labels to apply to the TKO test
+ keyvals: Keyval overrides to apply to the TKO test
+ bugs: Bugs filed that a relevant to this run
+ reason_override: Override for the AFE reason
+ """
+ condition = dbmodels.TextField()
+ enabled = dbmodels.BooleanField(default=False)
+
+ labels = dbmodels.ManyToManyField(tko_models.TestLabel, null=True,
+ db_table='planner_autoprocess_labels')
+ keyvals = dbmodels.ManyToManyField(KeyVal, null=True,
+ db_table='planner_autoprocess_keyvals')
+ bugs = dbmodels.ManyToManyField(Bug, null=True,
+ db_table='planner_autoprocess_bugs')
+ reason_override = dbmodels.CharField(max_length=255, null=True, blank=True)
+
+ class Meta:
+ db_table = 'planner_autoprocess'
+
+
+ def _get_details_unicode(self):
+ return 'Autoprocessing condition: %s' % self.condition
diff --git a/frontend/planner/models_test.py b/frontend/planner/models_test.py
new file mode 100644
index 0000000..cd95d39
--- /dev/null
+++ b/frontend/planner/models_test.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+
+import unittest
+import common
+from autotest_lib.frontend import setup_django_environment
+from autotest_lib.frontend.afe import frontend_test_utils, rpc_utils
+from autotest_lib.frontend.planner import models
+
+
+class ModelWithHashTestBase(frontend_test_utils.FrontendTestMixin):
+ def setUp(self):
+ self._frontend_common_setup(fill_data=False)
+
+
+ def tearDown(self):
+ self._frontend_common_teardown()
+
+
+ def _model_class(self):
+ raise NotImplementedError('Subclasses must override _model_class()')
+
+
+ def _test_data(self):
+ raise NotImplementedError('Subclasses must override _test_data()')
+
+
+ def test_disallowed_operations(self):
+ def _call_create():
+ self._model_class().objects.create(**self._test_data())
+ self.assertRaises(Exception, _call_create)
+
+ model = self._model_class().objects.get_or_create(
+ **self._test_data())[0]
+ self.assertRaises(Exception, model.save)
+
+
+ def test_hash_field(self):
+ model = self._model_class().objects.get_or_create(
+ **self._test_data())[0]
+ self.assertNotEqual(model.id, None)
+ self.assertEqual(self._model_class()._compute_hash(**self._test_data()),
+ model.the_hash)
+
+
+class ControlFileTest(ModelWithHashTestBase, unittest.TestCase):
+ def _model_class(self):
+ return models.ControlFile
+
+
+ def _test_data(self):
+ return {'contents' : 'test_control'}
+
+
+class KeyValTest(ModelWithHashTestBase, unittest.TestCase):
+ def _model_class(self):
+ return models.KeyVal
+
+
+ def _test_data(self):
+ return {'key' : 'test_key',
+ 'value' : 'test_value'}
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/frontend/planner/rpc_interface.py b/frontend/planner/rpc_interface.py
new file mode 100644
index 0000000..b8ea992
--- /dev/null
+++ b/frontend/planner/rpc_interface.py
@@ -0,0 +1,8 @@
+"""\
+Functions to expose over the RPC interface.
+"""
+
+__author__ = 'jamesren@google.com (James Ren)'
+
+
+# Nothing yet
diff --git a/frontend/planner/urls.py b/frontend/planner/urls.py
new file mode 100644
index 0000000..6a3516b
--- /dev/null
+++ b/frontend/planner/urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls.defaults import *
+import common
+from autotest_lib.frontend import settings, urls_common
+
+pattern_list, debug_pattern_list = (
+ urls_common.generate_pattern_lists('frontend.planner',
+ 'TestPlannerClient'))
+
+if settings.DEBUG:
+ pattern_list += debug_pattern_list
+
+urlpatterns = patterns('', *pattern_list)
diff --git a/frontend/planner/views.py b/frontend/planner/views.py
new file mode 100644
index 0000000..092c56a
--- /dev/null
+++ b/frontend/planner/views.py
@@ -0,0 +1,24 @@
+import urllib2, sys, traceback, cgi
+
+import common
+from autotest_lib.frontend import views_common
+from autotest_lib.frontend.afe import rpc_handler
+from autotest_lib.frontend.planner import models, rpc_interface
+
+rpc_handler_obj = rpc_handler.RpcHandler((rpc_interface,),
+ document_module=rpc_interface)
+
+
+def handle_rpc(request):
+ return rpc_handler_obj.handle_rpc_request(request)
+
+
+def rpc_documentation(request):
+ return rpc_handler_obj.get_rpc_documentation()
+
+
+def model_documentation(request):
+ model_names = ('Plan', 'Host', 'ControlFile', 'Test', 'Job', 'KeyVal',
+ 'Bug', 'TestRun', 'DataType', 'History', 'SavedObject',
+ 'CustomQuery', 'AutoProcess')
+ return views_common.model_documentation(models, model_names)
diff --git a/frontend/settings.py b/frontend/settings.py
index 7001afc..43e5eb7 100644
--- a/frontend/settings.py
+++ b/frontend/settings.py
@@ -43,6 +43,7 @@
# prefix applied to all URLs - useful if requests are coming through apache,
# and you need this app to coexist with others
URL_PREFIX = 'afe/server/'
+PLANNER_URL_PREFIX = 'planner/server/'
# Local time zone for this installation. Choices can be found here:
# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
@@ -105,6 +106,7 @@
INSTALLED_APPS = (
'frontend.afe',
+ 'frontend.planner',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
diff --git a/frontend/urls.py b/frontend/urls.py
index 74be84d..2f113b1 100644
--- a/frontend/urls.py
+++ b/frontend/urls.py
@@ -6,12 +6,14 @@
admin.autodiscover()
RE_PREFIX = '^' + settings.URL_PREFIX
+PLANNER_RE_PREFIX = '^' + settings.PLANNER_URL_PREFIX
handler500 = 'frontend.afe.views.handler500'
pattern_list = (
(RE_PREFIX + r'admin/(.*)', admin.site.root),
(RE_PREFIX, include('frontend.afe.urls')),
+ (PLANNER_RE_PREFIX, include('frontend.planner.urls')),
)
debug_pattern_list = (
diff --git a/frontend/urls_common.py b/frontend/urls_common.py
new file mode 100644
index 0000000..d9636ba
--- /dev/null
+++ b/frontend/urls_common.py
@@ -0,0 +1,36 @@
+import os
+
+
+def generate_pattern_lists(django_name, gwt_name):
+ """
+ Generates the common URL patterns for the given names
+
+ @param django_name the full name of the Django application
+ (e.g., frontend.afe)
+ @param gwt_name the name of the GWT project (e.g., AfeClient)
+ @return the common standard and the debug pattern lists, as a tuple
+ """
+
+ pattern_list = [
+ (r'^(?:|noauth/)rpc/', '%s.views.handle_rpc' % django_name),
+ (r'^rpc_doc', '%s.views.rpc_documentation' % django_name)
+ ]
+
+ debug_pattern_list = [
+ (r'^model_doc/', '%s.views.model_documentation' % django_name),
+
+ # for GWT hosted mode
+ (r'^(?P<forward_addr>autotest.*)',
+ 'autotest_lib.frontend.afe.views.gwt_forward'),
+
+ # for GWT compiled files
+ (r'^client/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': os.path.join(os.path.dirname(__file__), '..',
+ 'frontend', 'client', 'www')}),
+ # redirect / to compiled client
+ (r'^$', 'django.views.generic.simple.redirect_to',
+ {'url':
+ 'client/autotest.%(name)s/%(name)s.html' % dict(name=gwt_name)}),
+ ]
+
+ return (pattern_list, debug_pattern_list)
diff --git a/frontend/views_common.py b/frontend/views_common.py
new file mode 100644
index 0000000..14ac1e3
--- /dev/null
+++ b/frontend/views_common.py
@@ -0,0 +1,9 @@
+from django.http import HttpResponse
+
+def model_documentation(models_module, model_names):
+ doc = '<h2>Models</h2>\n'
+ for model_name in model_names:
+ model_class = getattr(models_module, model_name)
+ doc += '<h3>%s</h3>\n' % model_name
+ doc += '<pre>\n%s</pre>\n' % model_class.__doc__
+ return HttpResponse(doc)
diff --git a/new_tko/tko/urls.py b/new_tko/tko/urls.py
index 4c4d10e..fc64e58 100644
--- a/new_tko/tko/urls.py
+++ b/new_tko/tko/urls.py
@@ -1,30 +1,15 @@
from django.conf.urls.defaults import *
-from django.conf import settings
-import os
+import common
+from autotest_lib.frontend import settings, urls_common
-pattern_list = [(r'^(?:|noauth/)rpc/', 'new_tko.tko.views.handle_rpc'),
- (r'^(?:|noauth/)jsonp_rpc/',
- 'new_tko.tko.views.handle_jsonp_rpc'),
- (r'^(?:|noauth/)csv/', 'new_tko.tko.views.handle_csv'),
- (r'^rpc_doc', 'new_tko.tko.views.rpc_documentation'),
- (r'^(?:|noauth/)plot/', 'new_tko.tko.views.handle_plot')]
+pattern_list, debug_pattern_list = (
+ urls_common.generate_pattern_lists(django_name='new_tko.tko',
+ gwt_name='TkoClient'))
-debug_pattern_list = [
- (r'^model_doc/', 'new_tko.tko.views.model_documentation'),
-
- # for GWT hosted mode
- (r'^(?P<forward_addr>autotest.*)',
- 'autotest_lib.frontend.afe.views.gwt_forward'),
-
- # for GWT compiled files
- (r'^client/(?P<path>.*)$', 'django.views.static.serve',
- {'document_root': os.path.join(os.path.dirname(__file__), '..', '..',
- 'frontend', 'client', 'www')}),
- # redirect / to compiled client
- (r'^$', 'django.views.generic.simple.redirect_to',
- {'url': 'client/autotest.TkoClient/TkoClient.html'}),
-
-]
+pattern_list += [(r'^(?:|noauth/)jsonp_rpc/',
+ 'new_tko.tko.views.handle_jsonp_rpc'),
+ (r'^(?:|noauth/)csv/', 'new_tko.tko.views.handle_csv'),
+ (r'^(?:|noauth/)plot/', 'new_tko.tko.views.handle_plot')]
if settings.DEBUG:
pattern_list += debug_pattern_list