Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 132 additions & 10 deletions openhtf/output/servers/station_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import asyncio
import contextlib
import csv
import io
import itertools
import json
import logging
Expand Down Expand Up @@ -642,18 +644,26 @@ def get(self):
raise ValueError('history_path is None, try calling initialize() first')

for file_name in os.listdir(self.history_path):
if not file_name.endswith('.pb'):
continue
if not os.path.isfile(os.path.join(self.history_path, file_name)):
continue

dut_id = None
start_time_millis = None
match = re.match(r'mfg_event_(.+)_(\d+)\.pb$', file_name)

if match is not None:
dut_id = match.group(1)
start_time_millis = int(match.group(2))
if file_name.endswith('.pb'):
match = re.match(r'mfg_event_(.+)_(\d+)\.pb$', file_name)
if match is not None:
dut_id = match.group(1)
start_time_millis = int(match.group(2))
elif file_name.endswith('.json'):
match = re.match(
r'([^-]+)-(.+)-(PASS|FAIL|ERROR|TIMEOUT|ABORTED)-(\d+)\.json$',
file_name)
if match is not None:
dut_id = match.group(2)
start_time_millis = int(match.group(4))
else:
continue

if filter_dut_id and dut_id not in filter_dut_id:
continue
Expand All @@ -676,10 +686,19 @@ class HistoryItemHandler(BaseHistoryHandler):
"""GET endpoint for a test record from the history."""

def get(self, file_name):
# TODO(kenadia): Implement the history item handler. The implementation
# depends on the format used to store test records on disk.
self.write('Not implemented.')
self.set_status(500)
file_path = os.path.join(self.history_path, file_name)
if not os.path.isfile(file_path):
self.set_status(404)
self.write('File not found.')
return

if file_name.endswith('.json'):
with open(file_path, 'r') as f:
test_record_dict = json.load(f)
self.write(_test_state_from_record(test_record_dict))
else:
self.write('Unsupported file format.')
self.set_status(400)


class HistoryAttachmentsHandler(BaseHistoryHandler):
Expand All @@ -698,6 +717,106 @@ def get(self, file_name, attachment_name):
self.set_status(500)


class HistoryExportHandler(BaseHistoryHandler):
"""POST endpoint to export selected history items as a CSV of measurements."""

def set_default_headers(self):
super().set_default_headers()
self.set_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.set_header('Access-Control-Expose-Headers', 'Content-Disposition')

def _resolve_file_name(self, start_time_millis):
"""Find a JSON report file containing the given start_time_millis."""
if start_time_millis is None:
return None
start_time_str = str(int(start_time_millis))
for file_name in os.listdir(self.history_path):
if file_name.endswith('.json') and start_time_str in file_name:
return file_name
_LOG.warning('Could not resolve file for start_time_millis=%s in %s',
start_time_str, self.history_path)
return None

def post(self):
try:
body = json.loads(self.request.body.decode('utf-8'))
except ValueError:
self.set_status(400)
self.write('Invalid JSON body.')
return

file_names = body.get('file_names', [])
identifiers = body.get('identifiers', [])

for ident in identifiers:
resolved = self._resolve_file_name(ident.get('start_time_millis'))
if resolved:
file_names.append(resolved)

if not file_names:
self.set_status(400)
self.write('No files selected.')
return

rows = []
all_measurement_names = []
seen_measurement_names = set()
test_name = None
station_id = None

for file_name in file_names:
file_path = os.path.join(self.history_path, file_name)
if not os.path.isfile(file_path):
continue

with open(file_path, 'r') as f:
record = json.load(f)

if test_name is None:
test_name = record.get('metadata', {}).get('test_name', 'export')
if station_id is None:
station_id = record.get('station_id', 'unknown')

git_version = (record.get('metadata', {})
.get('additional', {})
.get('version_info', {})
.get('deployed_hash', ''))

row = {
'dut_id': record.get('dut_id', ''),
'start_time_millis': record.get('start_time_millis', ''),
'git_version': git_version,
}

for phase in record.get('phases', []):
for m_name, m_data in phase.get('measurements', {}).items():
if 'measured_value' in m_data:
row[m_name] = m_data['measured_value']
if m_name not in seen_measurement_names:
seen_measurement_names.add(m_name)
all_measurement_names.append(m_name)

rows.append(row)

meta_cols = ['dut_id', 'start_time_millis', 'git_version']
header = meta_cols + all_measurement_names

output = io.StringIO()
writer = csv.writer(output)
writer.writerow(header)
for row in rows:
writer.writerow([row.get(col, '') for col in header])

csv_data = output.getvalue()

now_millis = int(time.time() * 1000)
filename = f'{test_name or "export"}-{station_id or "unknown"}-{now_millis}.csv'

self.set_header('Content-Type', 'text/csv')
self.set_header('Content-Disposition', f'attachment; filename="{filename}"')
self.write(csv_data)


class StationMulticast(multicast.MulticastListener):
"""Announce the existence of a station server to any searching dashboards."""

Expand Down Expand Up @@ -802,6 +921,9 @@ def __init__(
(r'/history', HistoryListHandler, {
'history_path': history_path
}),
(r'/history/export', HistoryExportHandler, {
'history_path': history_path
}),
(r'/history/(?P<file_name>[^/]+)', HistoryItemHandler, {
'history_path': history_path
}),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions openhtf/output/web_gui/dist/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<head><link href="/css/app.575e9bb61286fde76cb9.css" rel="stylesheet"></head><!doctype html>
<head><link href="/css/app.587dc395b04f17af29f3.css" rel="stylesheet"></head><!doctype html>
<!--
Copyright 2022 Google LLC

Expand All @@ -22,4 +22,4 @@

<base href="/">
<htf-app config="{{ json_encode(config) }}">Loading...</htf-app>
<script type="text/javascript" src="/js/polyfills.575e9bb61286fde76cb9.js"></script><script type="text/javascript" src="/js/vendor.575e9bb61286fde76cb9.js"></script><script type="text/javascript" src="/js/app.575e9bb61286fde76cb9.js"></script>
<script type="text/javascript" src="/js/polyfills.587dc395b04f17af29f3.js"></script><script type="text/javascript" src="/js/vendor.587dc395b04f17af29f3.js"></script><script type="text/javascript" src="/js/app.587dc395b04f17af29f3.js"></script>

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class HistoryItem {
startTimeMillis: number|null;
status: HistoryItemStatus;
testState: TestState|null; // Null if status is not `loaded`.
selected = false; // Whether selected for CSV export.

constructor(params: HistoryItemParams) {
Object.assign(this, params);
Expand Down
Loading