Bug 1050552 - Allow storage of multiple dashboards at one URL;r=davehunt

This commit is contained in:
William Lachance 2014-08-13 17:21:47 -04:00
Родитель 4ddc6803aa
Коммит 5de4435bd5
6 изменённых файлов: 299 добавлений и 177 удалений

Просмотреть файл

@ -7,6 +7,7 @@ import optparse
import os
import requests
import sys
import urlparse
exit_status = 0
MAX_WORKERS = 8
@ -43,6 +44,11 @@ def validate_json_response(r):
return True
def urljoin(*args):
baseurl = args[0]
path = "/".join(args[1:])
return urlparse.urljoin(baseurl, path)
def download_file(url, filename):
r = requests.get(url)
if not validate_response(r):
@ -77,21 +83,25 @@ def download_testdata(url, baseurl, filename, options, metadatadir,
videodir, profiledir):
r = requests.get(url)
if not validate_json_response(r):
print "WARNING: %s json data invalid" % url
return
open(filename, 'w').write(r.content)
testdata = r.json()['testdata']
with concurrent.futures.ThreadPoolExecutor(MAX_WORKERS) as executor:
testdata = r.json()['testdata']
for appname in testdata.keys():
for date in testdata[appname].keys():
for datapoint in testdata[appname][date]:
uuid = datapoint['uuid']
if options.download_metadata:
metadata_filename = "%s.json" % uuid
executor.submit(download_metadata,
baseurl + 'metadata/%s.json' % uuid,
urljoin(baseurl, 'metadata',
metadata_filename),
baseurl,
os.path.join(metadatadir,
'%s.json' % uuid),
metadata_filename),
options, videodir,
profiledir)
@ -103,9 +113,13 @@ parser.add_option("--full-mirror", action="store_true",
parser.add_option("--skip-metadata", action="store_false",
dest="download_metadata", default=True,
help="Skip downloading metadata JSON files")
parser.add_option("--dashboard-id", action="store",
dest="dashboard_id",
help="Only download information for dashboard id")
parser.add_option("--device-id", action="store",
dest="device_id",
help="Only download information for device id")
help="Only download information for device id (must be used "
"in conjunction with --dashboard-id)")
parser.add_option("--rewrite-metadata", action="store_true",
dest="rewrite_metadata", default=False,
help="Rewrite metadata to use absolute URLs to original "
@ -117,7 +131,11 @@ if len(args) != 2:
sys.exit(1)
if options.full_mirror and not options.download_metadata:
parser.error("ERROR: Need to download metadata for full mirror")
parser.error("Need to download metadata for full mirror")
sys.exit(1)
if options.device_id and not options.dashboard_id:
parser.error("--device-id must be used in conjunction with --dashboard-id")
sys.exit(1)
(baseurl, outputdir) = args
@ -130,44 +148,69 @@ metadatadir = os.path.join(outputdir, 'metadata')
videodir = os.path.join(outputdir, 'videos')
profiledir = os.path.join(outputdir, 'profiles')
r = requests.get(baseurl + 'devices.json')
r = requests.get(urljoin(baseurl, 'dashboard.json'))
if not validate_json_response(r):
print "Can't download device list, exiting"
print "Can't download dashboard list, exiting"
sys.exit(1)
devicedict = r.json()
save_file(os.path.join(outputdir, 'dashboard.json'), r.content)
save_file(os.path.join(outputdir, 'devices.json'), r.content)
if options.device_id:
if options.device_id in r.json()['devices'].keys():
deviceids = [ options.device_id ]
dashboard_ids = map(lambda d: d['id'], r.json()['dashboards'])
if options.dashboard_id:
if options.dashboard_id in dashboard_ids:
dashboard_ids = [ options.dashboard_id ]
else:
print "WARNING: Device id '%s' specified but unavailable. Skipping." % \
options.device_id
deviceids = []
else:
deviceids = devicedict['devices'].keys()
print "ERROR: dashboard id '%s' specified but unavailable." % \
options.dashboard_id
with concurrent.futures.ThreadPoolExecutor(MAX_WORKERS) as executor:
for deviceid in deviceids:
for branchid in devicedict['devices'][deviceid]['branches']:
r = requests.get(baseurl + '%s/%s/tests.json' % (deviceid, branchid))
if not validate_json_response(r):
print "Skipping tests for device: %s, branch: %s" % (
deviceid, branchid)
continue
for dashboard_id in dashboard_ids:
dashboard_dir = os.path.join(outputdir, dashboard_id)
create_dir(dashboard_dir)
testdir = os.path.join(outputdir, deviceid, branchid)
create_dir(testdir)
save_file(os.path.join(testdir, 'tests.json'), r.content)
testnames = r.json()['tests'].keys()
for testname in testnames:
executor.submit(download_testdata,
baseurl + '%s/%s/%s.json' % (deviceid, branchid, testname),
baseurl,
os.path.join(outputdir, deviceid, branchid,
'%s.json' % testname),
options,
metadatadir, videodir, profiledir)
r = requests.get(urljoin(baseurl, dashboard_id, 'devices.json'))
if not validate_json_response(r):
print "ERROR: Can't download device list for dashboard '%s', " \
"exiting" % dashboard_id
sys.exit(1)
save_file(os.path.join(dashboard_dir, 'devices.json'), r.content)
devices = r.json()['devices']
if options.device_id:
if options.device_id in devices.keys():
deviceids = [ options.device_id ]
else:
print "WARNING: Device id '%s' specified but unavailable. " \
"Skipping." % options.device_id
deviceids = []
else:
deviceids = devices.keys()
for deviceid in deviceids:
for branchid in devices[deviceid]['branches']:
r = requests.get(urljoin(baseurl, dashboard_id, deviceid,
branchid, 'tests.json'))
if not validate_json_response(r):
print "WARNING: Skipping tests for dashboard %s, " \
"device: %s, branch: %s" % (dashboard_id, deviceid,
branchid)
continue
testdir = os.path.join(dashboard_dir, deviceid, branchid)
create_dir(testdir)
save_file(os.path.join(testdir, 'tests.json'), r.content)
tests = r.json()['tests']
for testname in tests.keys():
testfilename = '%s.json' % testname
executor.submit(download_testdata,
urljoin(baseurl, dashboard_id, deviceid,
branchid, testfilename),
baseurl,
os.path.join(outputdir, dashboard_id,
deviceid, branchid,
testfilename),
options,
metadatadir, videodir, profiledir)
sys.exit(exit_status)

Просмотреть файл

@ -118,6 +118,7 @@ def runtest(dm, device_prefs, options, product, appinfo, testinfo,
# Write testdata
eideticker.update_dashboard_testdata(options.dashboard_dir,
options.dashboard_id,
options.device_id,
options.branch_id, testinfo,
productname, appdate,
@ -131,6 +132,12 @@ def main(args=sys.argv[1:]):
parser.add_option("--enable-profiling",
action="store_true", dest="enable_profiling",
help="Create SPS profile to go along with capture")
parser.add_option("--dashboard-id", action="store", dest="dashboard_id",
help="id of dashboard (used in output json)",
default=os.environ.get('DASHBOARD_ID'))
parser.add_option("--dashboard-name", action="store", dest="dashboard_name",
help="name of dashboard to display",
default=os.environ.get('DASHBOARD_NAME'))
parser.add_option("--device-id", action="store", dest="device_id",
help="id of device (used in output json)",
default=os.environ.get('DEVICE_ID'))
@ -166,14 +173,19 @@ def main(args=sys.argv[1:]):
parser.print_usage()
sys.exit(1)
if not options.dashboard_id:
parser.error("Must specify dashboard id (either with --dashboard-id "
"or with DASHBOARD_ID environment variable)")
if not options.dashboard_name:
parser.error("Must specify dashboard name (either with "
"--dashboard-name or with DASHBOARD_NAME environment "
"varaiable)")
if not options.device_id:
print "ERROR: Must specify device id (either with --device-id or with " \
"DEVICE_ID environment variable)"
sys.exit(1)
parser.error("Must specify device id (either with --device-id or with "
"DEVICE_ID environment variable)")
if not options.branch_id:
print "ERROR: Must specify branch (either with --branch or with " \
"BRANCH environment variable)"
sys.exit(1)
parser.error("Must specify branch (either with --branch or with "
"BRANCH environment variable)")
# get device info
device_prefs = eideticker.getDevicePrefs(options)
@ -195,9 +207,11 @@ def main(args=sys.argv[1:]):
else:
print "ERROR: Unknown device type '%s'" % options.devicetype
# update device index
eideticker.update_dashboard_device_list(options.dashboard_dir, options.device_id,
options.branch_id, device_info)
# update dashboard / device index
eideticker.update_dashboard_list(options.dashboard_dir, options.dashboard_id,
options.dashboard_name)
eideticker.update_dashboard_device_list(options.dashboard_dir, options.dashboard_id,
options.device_id, options.branch_id, device_info)
# get application/build info
if options.devicetype == "android":
@ -236,8 +250,8 @@ def main(args=sys.argv[1:]):
for testkey in args:
testinfo = eideticker.get_testinfo(testkey)
eideticker.update_dashboard_test_list(options.dashboard_dir, options.device_id,
options.branch_id,
eideticker.update_dashboard_test_list(options.dashboard_dir, options.dashboard_id,
options.device_id, options.branch_id,
testinfo)
current_date = time.strftime("%Y-%m-%d")

Просмотреть файл

@ -89,19 +89,21 @@
<body>
<div class="navbar navbar-default navbar-static-top">
<div class="navbar-header">
<a class="navbar-brand">Eideticker Dashboard</a>
</div>
<div class="navbar-header">
<a class="navbar-brand">Eideticker Dashboard</a>
</div>
<ul id="dashboard-chooser" class="nav navbar-nav">
</ul>
</div>
<div id="chooser">
<div class="list-group" id="device-chooser"></div>
<div class="list-group" id="branch-chooser"></div>
<div class="list-group" id="test-chooser"></div>
</div>
<div id="chooser">
<div class="list-group" id="device-chooser"></div>
<div class="list-group" id="branch-chooser"></div>
<div class="list-group" id="test-chooser"></div>
</div>
<div id="data-view">
</div>
<div id="data-view">
</div>
<footer id="footer">
<p>Built by <a href="http://wiki.mozilla.org/Auto-tools">Mozilla Firefox Automation & Tools Engineering</a>. Source available on <a href="https://github.com/mozilla/eideticker">github</a>. Bug or feature request? File it on <a href="http://bugzilla.mozilla.org">bugzilla</a> in the <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Eideticker">Eideticker component</a>.</p>

Просмотреть файл

@ -8,8 +8,9 @@ function getResourceURL(path) {
return serverPrefix + path;
}
function updateContent(testInfo, deviceId, branchId, testId, measureId, timeRange) {
$.getJSON(getResourceURL([deviceId, branchId, testId].join('/') + '.json'), function(dict) {
function updateContent(testInfo, dashboardId, deviceId, branchId, testId, measureId, timeRange) {
$.getJSON(getResourceURL([dashboardId, deviceId, branchId,
testId].join('/') + '.json'), function(dict) {
if (!dict || !dict['testdata']) {
$('#data-view').html(ich.noGraph({
"title": testInfo.shortDesc,
@ -88,7 +89,7 @@ function updateContent(testInfo, deviceId, branchId, testId, measureId, timeRang
$('#measure-'+measureId).attr("selected", "true");
$('#measure').change(function() {
var newMeasureId = $(this).val();
window.location.hash = '/' + [ deviceId, branchId, testId,
window.location.hash = '/' + [ dashboardId, deviceId, branchId, testId,
newMeasureId, timeRange ].join('/');
});
}
@ -97,10 +98,9 @@ function updateContent(testInfo, deviceId, branchId, testId, measureId, timeRang
$('#time-range-' + timeRange).attr("selected", "true");
$('#time-range').change(function() {
var newTimeRange = $(this).val();
window.location.hash = '/' + [ deviceId, branchId, testId,
window.location.hash = '/' + [ dashboardId, deviceId, branchId, testId,
measureId, newTimeRange ].join('/');
});
});
}
@ -372,91 +372,138 @@ function updateGraph(title, rawdata, measureId) {
}
$(function() {
var graphData = {};
var dashboards;
var devices;
var deviceIds;
var currentDashboardId;
$.getJSON(getResourceURL('devices.json'), function(deviceData) {
var devices = deviceData['devices'];
var deviceIds = Object.keys(devices).sort();
function getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest) {
var device = devices[deviceId];
var tests = device[branchId].tests;
var testIds = Object.keys(tests).sort();
var testId;
if (preferredTest && testIds.indexOf(preferredTest) >= 0) {
// this deviceid/branch combo has our preferred test, return it
return preferredTest;
} else {
// this deviceid/branch combo *doesn't* have our preferred test,
// fall back to the first one
return testIds[0];
}
}
var deviceBranchPairs = [];
deviceIds.forEach(function(deviceId) {
devices[deviceId].branches.forEach(function(branchId) {
deviceBranchPairs.push([deviceId, branchId]);
});
function updateDashboardChooser() {
$('#dashboard-chooser').empty();
dashboards.forEach(function(dashboard) {
$('<li id="dashboard-' + dashboard.id + '"><a href="' + '#/' +
dashboard.id + '">' + dashboard.name +
"</a></li>").appendTo($('#dashboard-chooser'));
});
$.when.apply($, deviceBranchPairs.map(function(pair) {
var deviceId = pair[0];
var branchId = pair[1];
$("#dashboard-" + currentDashboardId).addClass("active");
}
return $.getJSON(getResourceURL([deviceId, branchId, 'tests.json'].join('/')),
function(testData) {
var tests = testData['tests'];
if (!devices[deviceId][branchId]) {
devices[deviceId][branchId] = {};
}
devices[deviceId][branchId]['tests'] = tests;
});
})).done(function() {
var defaultDeviceId = deviceIds[0];
function updateDeviceChooser(timeRange, preferredBranchId, preferredTest) {
$('#device-chooser').empty();
function getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest) {
var device = devices[deviceId];
var tests = device[branchId].tests;
var testIds = Object.keys(tests).sort();
var testId;
if (preferredTest && testIds.indexOf(preferredTest) >= 0) {
// this deviceid/branch combo has our preferred test, return it
return preferredTest;
} else {
// this deviceid/branch combo *doesn't* have our preferred test,
// fall back to the first one
return testIds[0];
deviceIds.forEach(function(deviceId) {
var device = devices[deviceId];
var branchId;
if (preferredBranchId && device.branches.indexOf(preferredBranchId) >= 0) {
branchId = preferredBranchId;
} else {
branchId = device.branches.sort()[0];
}
var testId = getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest);
var defaultMeasureId = device[branchId].tests[testId].defaultMeasureId;
var deviceURL = "#/" + [ currentDashboardId, deviceId, branchId, testId, defaultMeasureId, timeRange ].join('/');
$('<a href="' + deviceURL + '" id="device-' + deviceId + '" deviceid= ' + deviceId + ' class="list-group-item">' + devices[deviceId].name+'</a></li>').appendTo(
$('#device-chooser'));
});
}
function updateBranchChooser(timeRange, deviceId, preferredTest) {
$('#branch-chooser').empty();
var device = devices[deviceId];
device.branches.forEach(function(branchId) {
var testId = getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest);
var defaultMeasureId = device[branchId].tests[testId].defaultMeasureId;
var url = "#/" + [ currentDashboardId, deviceId, branchId, testId, defaultMeasureId, timeRange ].join('/');
$('<a href="' + url + '" id="branch-' + branchId + '" class="list-group-item">' + branchId +'</a></li>').appendTo(
$('#branch-chooser'));
});
}
function updateDashboard(dashboardId, cb) {
if (currentDashboardId === dashboardId) {
cb();
return;
}
currentDashboardId = dashboardId;
$.getJSON(getResourceURL([dashboardId, 'devices.json'].join('/')), function(deviceData) {
devices = deviceData['devices'];
deviceIds = Object.keys(devices).sort();
var deviceBranchPairs = [];
deviceIds.forEach(function(deviceId) {
devices[deviceId].branches.forEach(function(branchId) {
deviceBranchPairs.push([deviceId, branchId]);
});
});
$.when.apply($, deviceBranchPairs.map(function(pair) {
var deviceId = pair[0];
var branchId = pair[1];
return $.getJSON(getResourceURL([dashboardId, deviceId, branchId,
'tests.json'].join('/')),
function(testData) {
var tests = testData['tests'];
if (!devices[deviceId][branchId]) {
devices[deviceId][branchId] = {};
}
devices[deviceId][branchId]['tests'] = tests;
});
})).done(cb);
});
}
$.getJSON(getResourceURL('dashboard.json'), function(dashboardData) {
dashboards = dashboardData.dashboards.sort(function(x, y) {
return y.id < x.id;
});
var routes = {
'/:dashboardId': {
on: function(dashboardId) {
updateDashboard(dashboardId, function() {
updateDashboardChooser();
var defaultDeviceId = deviceIds[0];
var defaultBranchId = devices[defaultDeviceId].branches[0];
var defaultTestId = Object.keys(devices[defaultDeviceId][defaultBranchId].tests).sort()[0];
var defaultMeasureId = devices[defaultDeviceId][defaultBranchId].tests[defaultTestId].defaultMeasureId;
var defaultTimeRange = 7;
window.location.hash = '/' + [ currentDashboardId, defaultDeviceId,
defaultBranchId,
defaultTestId,
defaultMeasureId,
defaultTimeRange ].join('/');
});
}
}
},
'/:dashboardId/:deviceId/:branchId/:testId/:measureId/:timeRange': {
on: function(dashboardId, deviceId, branchId, testId, measureId, timeRange) {
updateDashboard(dashboardId, function() {
updateDashboardChooser();
function updateDeviceChooser(timeRange, preferredBranchId, preferredTest) {
$('#device-chooser').empty();
deviceIds.forEach(function(deviceId) {
var device = devices[deviceId];
var branchId;
if (preferredBranchId && device.branches.indexOf(preferredBranchId) >= 0) {
branchId = preferredBranchId;
} else {
branchId = device.branches.sort()[0];
}
var testId = getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest);
var defaultMeasureId = device[branchId].tests[testId].defaultMeasureId;
var deviceURL = "#/" + [ deviceId, branchId, testId, defaultMeasureId, timeRange ].join('/');
$('<a href="' + deviceURL + '" id="device-' + deviceId + '" deviceid= ' + deviceId + ' class="list-group-item">' + devices[deviceId].name+'</a></li>').appendTo(
$('#device-chooser'));
});
}
function updateBranchChooser(timeRange, deviceId, preferredTest) {
$('#branch-chooser').empty();
var device = devices[deviceId];
device.branches.forEach(function(branchId) {
var testId = getTestIdForDeviceAndBranch(deviceId, branchId, preferredTest);
var defaultMeasureId = device[branchId].tests[testId].defaultMeasureId;
var url = "#/" + [ deviceId, branchId, testId, defaultMeasureId, timeRange ].join('/');
$('<a href="' + url + '" id="branch-' + branchId + '" class="list-group-item">' + branchId +'</a></li>').appendTo(
$('#branch-chooser'));
});
}
var currentBranchId = null;
var currentDeviceId = null;
var currentTestId = null;
var routes = {
'/:deviceId/:branchId/:testId/:measureId/:timeRange': {
on: function(deviceId, branchId, testId, measureId, timeRange) {
if (!devices[deviceId] || !devices[deviceId][branchId] || !devices[deviceId][branchId]['tests'][testId]) {
$('#data-view').html("<p class='lead'>That device/branch/test/measure combination does not seem to exist. Maybe you're using an expired link? <a href=''>Reload page</a>?</p>");
return;
@ -481,8 +528,9 @@ $(function() {
if (testIdAttr) {
var defaultMeasureId = tests[testIdAttr].defaultMeasureId;
$(this).attr('href', '#/' +
[ deviceId, branchId, testIdAttr,
defaultMeasureId, timeRange ].join('/'));
[ currentDashboardId, deviceId, branchId,
testIdAttr, defaultMeasureId,
timeRange ].join('/'));
}
});
@ -493,34 +541,24 @@ $(function() {
var testInfo = tests[testId];
updateFooter();
updateContent(testInfo, deviceId, branchId, testId, measureId, timeRange);
}
updateContent(testInfo, dashboardId, deviceId, branchId, testId,
measureId, timeRange);
});
}
};
}
}
var defaultDeviceId = deviceIds[0];
var defaultBranchId = devices[defaultDeviceId].branches[0];
var defaultTestId = Object.keys(devices[defaultDeviceId][defaultBranchId].tests).sort()[0];
var defaultMeasureId = devices[defaultDeviceId][defaultBranchId].tests[defaultTestId].defaultMeasureId;
var defaultTimeRange = 7;
var defaultRouteHash = '/' + [ defaultDeviceId,
defaultBranchId,
defaultTestId,
defaultMeasureId,
defaultTimeRange ].join('/');
var router = Router(routes).configure({
'notfound': function() {
$('#data-view').html(ich.noGraph({
"title": "Invalid URL",
"errorReason": "Invalid or expired route (probably due to a " +
"change in Eideticker). Try selecting a valid combination " +
"from the menu on the left."
}));
}
});
router.init(defaultRouteHash);
var router = Router(routes).configure({
'notfound': function() {
$('#data-view').html(ich.noGraph({
"title": "Invalid URL",
"errorReason": "Invalid or expired route (probably due to a " +
"change in Eideticker). Try selecting a valid combination " +
"from the menu on the left."
}));
}
});
router.init('/'+ dashboards[0].id);
});
});

Просмотреть файл

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
from dashboard import add_dashboard_options, copy_dashboard_files, update_dashboard_device_list, update_dashboard_test_list, update_dashboard_testdata, upload_dashboard
from dashboard import add_dashboard_options, copy_dashboard_files, update_dashboard_list, update_dashboard_device_list, update_dashboard_test_list, update_dashboard_testdata, upload_dashboard
from runner import AndroidBrowserRunner
from options import OptionParser, CaptureOptionParser, TestOptionParser
from device import getDevicePrefs, getDevice

Просмотреть файл

@ -68,9 +68,33 @@ def copy_dashboard_files(dashboard_dir, indexfile='index.html'):
os.remove(dest)
shutil.copyfile(source, dest)
def update_dashboard_device_list(dashboard_dir, device_id, branch_id, device_info):
def update_dashboard_list(dashboard_dir, dashboard_id, dashboard_name):
dashboards = []
dashboard_filename = os.path.join(dashboard_dir, 'dashboard.json')
if os.path.isfile(dashboard_filename):
dashboards = json.loads(open(dashboard_filename).read())['dashboards']
found_dashboard = False
for dashboard in dashboards:
if dashboard['id'] == dashboard_id:
dashboard['name'] = dashboard_name
found_dashboard = True
if not found_dashboard:
dashboards.append({'id': dashboard_id, 'name': dashboard_name})
with open(dashboard_filename, 'w') as f:
f.write(json.dumps({'dashboards': dashboards}))
def update_dashboard_device_list(dashboard_dir, dashboard_id, device_id, branch_id,
device_info):
devices = {}
device_filename = os.path.join(dashboard_dir, 'devices.json')
device_dirname = os.path.join(dashboard_dir, dashboard_id)
if not os.path.exists(device_dirname):
os.makedirs(device_dirname)
device_filename = os.path.join(device_dirname, 'devices.json')
if os.path.isfile(device_filename):
devices = json.loads(open(device_filename).read())['devices']
@ -85,8 +109,8 @@ def update_dashboard_device_list(dashboard_dir, device_id, branch_id, device_inf
with open(device_filename, 'w') as f:
f.write(json.dumps({'devices': devices}))
def update_dashboard_test_list(dashboard_dir, device_id, branch_id, testinfo):
testsdirname = os.path.join(dashboard_dir, device_id, branch_id)
def update_dashboard_test_list(dashboard_dir, dashboard_id, device_id, branch_id, testinfo):
testsdirname = os.path.join(dashboard_dir, dashboard_id, device_id, branch_id)
if not os.path.exists(testsdirname):
os.makedirs(testsdirname)
@ -105,10 +129,11 @@ def update_dashboard_test_list(dashboard_dir, device_id, branch_id, testinfo):
'lastUpdated': update_timestamp,
'tests': tests}))
def update_dashboard_testdata(dashboard_dir, device_id, branch_id, testinfo,
productname, productdate, datapoint, metadata):
def update_dashboard_testdata(dashboard_dir, dashboard_id, device_id,
branch_id, testinfo, productname, productdate,
datapoint, metadata):
# get existing data
fname = os.path.join(dashboard_dir, device_id, branch_id,
fname = os.path.join(dashboard_dir, dashboard_id, device_id, branch_id,
'%s.json' % testinfo['key'])
testdata = NestedDict()
if os.path.isfile(fname):