mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-29 07:19:58 -04:00
Compare commits
4 Commits
471d159af7
...
ae669767c8
Author | SHA1 | Date | |
---|---|---|---|
ae669767c8 | |||
a84326cb67 | |||
c12c7c2477 | |||
03820d2941 |
@ -5,15 +5,16 @@
|
||||
query_type: Range # The type of graph. Range for timeseries and Scalar for point in time
|
||||
d3_tickformat: "~s" # Default tick format for the graph y axis
|
||||
legend_orientation: h
|
||||
yaxes:
|
||||
yaxes: # The yaxes definitions to feed to plotly.
|
||||
- anchor: "y"
|
||||
# overlaying: "y"
|
||||
side: left
|
||||
tickformat: "~%"
|
||||
# type: "log" # The type of axis.
|
||||
plots: # List of pluts to show on the graph
|
||||
- source: http://heimdall:9001 # Prometheus source uri for this plot
|
||||
query: 'sum by (instance)(irate(node_cpu_seconds_total{FILTERS, job="nodestats"}[5m]))' # The PromQL query for this plot
|
||||
meta: # metadata for this plot
|
||||
config: # configuration for this plot
|
||||
name_format: "`${labels.instance}`" # javascript template literal to format the trace name
|
||||
fill: tozeroy
|
||||
span: # The span for this range query
|
||||
@ -40,13 +41,13 @@
|
||||
# You can use the FILTERS placeholder to indicate where user selected filters should be placed.
|
||||
query: |
|
||||
sum by (instance)(irate(node_cpu_seconds_total{FILTERS mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{FILTERS, job="nodestats"}[5m]))
|
||||
meta:
|
||||
config:
|
||||
name_format: "`${labels.instance} system`"
|
||||
yaxis: "y"
|
||||
- source: http://heimdall:9001
|
||||
query: |
|
||||
sum by (instance)(irate(node_cpu_seconds_total{mode="user",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
|
||||
meta:
|
||||
config:
|
||||
name_format: "`${labels.instance} user`"
|
||||
yaxis: "y2"
|
||||
- title: Node memory
|
||||
@ -57,7 +58,7 @@
|
||||
plots:
|
||||
- source: http://heimdall:9001
|
||||
query: 'node_memory_MemFree_bytes{job="nodestats"}'
|
||||
meta:
|
||||
config:
|
||||
name_format: "`${labels.instance}`"
|
||||
- title: Log Test Dashboard 1
|
||||
span:
|
||||
|
@ -27,7 +27,7 @@ use crate::query::{
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PlotMeta {
|
||||
pub struct PlotConfig {
|
||||
name_format: Option<String>,
|
||||
fill: Option<FillTypes>,
|
||||
yaxis: Option<String>,
|
||||
@ -57,6 +57,22 @@ pub enum AxisSide {
|
||||
Left,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum AxisType {
|
||||
#[serde(rename = "-")]
|
||||
Default,
|
||||
#[serde(rename = "linear")]
|
||||
Linear,
|
||||
#[serde(rename = "log")]
|
||||
Log,
|
||||
#[serde(rename = "date")]
|
||||
Date,
|
||||
#[serde(rename = "category")]
|
||||
Category,
|
||||
#[serde(rename = "multicategory")]
|
||||
MultiCategory,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AxisDefinition {
|
||||
anchor: Option<String>,
|
||||
@ -64,6 +80,8 @@ pub struct AxisDefinition {
|
||||
side: Option<AxisSide>,
|
||||
#[serde(rename = "tickformat")]
|
||||
tick_format: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
plot_type: Option<AxisType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@ -86,7 +104,7 @@ pub struct Dashboard {
|
||||
pub struct SubPlot {
|
||||
pub source: String,
|
||||
pub query: String,
|
||||
pub meta: PlotMeta,
|
||||
pub config: PlotConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
@ -222,7 +240,7 @@ impl Graph {
|
||||
&plot.source,
|
||||
&plot.query,
|
||||
self.query_type.clone(),
|
||||
plot.meta.clone(),
|
||||
plot.config.clone(),
|
||||
);
|
||||
if let Some(filters) = filters {
|
||||
debug!(?filters, "query connection with filters");
|
||||
|
@ -16,7 +16,7 @@ use std::collections::HashMap;
|
||||
use chrono::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dashboard::PlotMeta;
|
||||
use crate::dashboard::PlotConfig;
|
||||
|
||||
mod loki;
|
||||
mod prom;
|
||||
@ -48,8 +48,8 @@ pub struct LogLine {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum MetricsQueryResult {
|
||||
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),
|
||||
Scalar(Vec<(HashMap<String, String>, PlotMeta, DataPoint)>),
|
||||
Series(Vec<(HashMap<String, String>, PlotConfig, Vec<DataPoint>)>),
|
||||
Scalar(Vec<(HashMap<String, String>, PlotConfig, DataPoint)>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -20,7 +20,7 @@ use prometheus_http_query::{
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::dashboard::PlotMeta;
|
||||
use crate::dashboard::PlotConfig;
|
||||
|
||||
use super::{DataPoint, MetricsQueryResult, QueryType, TimeSpan};
|
||||
|
||||
@ -35,7 +35,7 @@ pub struct PromQueryConn<'conn> {
|
||||
span: Option<TimeSpan>,
|
||||
query_type: QueryType,
|
||||
filters: Option<&'conn HashMap<&'conn str, &'conn str>>,
|
||||
pub meta: PlotMeta,
|
||||
pub meta: PlotConfig,
|
||||
}
|
||||
|
||||
impl<'conn> PromQueryConn<'conn> {
|
||||
@ -43,7 +43,7 @@ impl<'conn> PromQueryConn<'conn> {
|
||||
source: &'a str,
|
||||
query: &'a str,
|
||||
query_type: QueryType,
|
||||
meta: PlotMeta,
|
||||
meta: PlotConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
@ -159,7 +159,7 @@ impl<'conn> PromQueryConn<'conn> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prom_to_samples(data: Data, meta: PlotMeta) -> MetricsQueryResult {
|
||||
pub fn prom_to_samples(data: Data, meta: PlotConfig) -> MetricsQueryResult {
|
||||
match data {
|
||||
Data::Matrix(mut range) => MetricsQueryResult::Series(
|
||||
range
|
||||
|
@ -262,7 +262,7 @@ pub fn mk_ui_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
||||
fn graph_lib_prelude() -> Markup {
|
||||
html! {
|
||||
script src="/js/plotly.js" { }
|
||||
script type="module" defer src="/js/lib.js" { }
|
||||
script type="module" defer src="/js/lib.mjs" { }
|
||||
link rel="stylesheet" href="/static/site.css" { }
|
||||
}
|
||||
}
|
||||
@ -370,13 +370,13 @@ pub async fn plotly() -> Response<String> {
|
||||
}
|
||||
|
||||
pub async fn lib() -> Response<String> {
|
||||
javascript_response(include_str!("../static/lib.js"))
|
||||
javascript_response(include_str!("../static/lib.mjs"))
|
||||
}
|
||||
|
||||
pub fn mk_js_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
||||
Router::new()
|
||||
.route("/plotly.js", get(plotly))
|
||||
.route("/lib.js", get(lib))
|
||||
.route("/lib.mjs", get(lib))
|
||||
.route("/htmx.js", get(htmx))
|
||||
.with_state(State(config))
|
||||
}
|
||||
|
@ -83,7 +83,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PlotMeta
|
||||
* @typedef PlotConfig
|
||||
* @type {object}
|
||||
* @property {string=} name_format
|
||||
* @property {string=} yaxis
|
||||
|
@ -195,7 +195,7 @@ export class GraphPlot extends HTMLElement {
|
||||
self.stopInterval()
|
||||
self.fetchData().then((data) => {
|
||||
if (!updateOnly) {
|
||||
self.getLabelsForData(data.Metrics || data.Logs.Lines);
|
||||
self.getLabelsForData(data.Metrics || data.Logs.lines);
|
||||
self.buildFilterMenu();
|
||||
}
|
||||
self.updateGraph(data).then(() => {
|
||||
@ -252,13 +252,13 @@ export class GraphPlot extends HTMLElement {
|
||||
|
||||
/**
|
||||
* Formats the name for the plot trace.
|
||||
* @param {PlotMeta} meta
|
||||
* @param {PlotConfig} config
|
||||
* @param {Map<string, string>} labels
|
||||
* @return string
|
||||
*/
|
||||
formatName(meta, labels) {
|
||||
formatName(config, labels) {
|
||||
var name = "";
|
||||
const formatter = meta.name_format
|
||||
const formatter = config.name_format
|
||||
if (formatter) {
|
||||
name = eval(formatter);
|
||||
} else {
|
||||
@ -271,6 +271,10 @@ export class GraphPlot extends HTMLElement {
|
||||
return name;
|
||||
}
|
||||
|
||||
getFilterLabels() {
|
||||
return this.#filterLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object<string, string>} labels
|
||||
*/
|
||||
@ -365,6 +369,32 @@ export class GraphPlot extends HTMLElement {
|
||||
* @param {QueryData|LogLineList} graph
|
||||
*/
|
||||
getLabelsForData(graph) {
|
||||
if (/** @type {QueryData} */(graph).plots) {
|
||||
this.getLabelsForQueryData(/** @type {QueryData} */(graph));
|
||||
} else {
|
||||
this.getLabelsForLogLines(/** @type {LogLineList} */(graph));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LogLineList} graph
|
||||
*/
|
||||
getLabelsForLogLines(graph) {
|
||||
if (graph.Stream) {
|
||||
for (const pair of graph.Stream) {
|
||||
const labels = pair[0];
|
||||
this.populateFilterData(labels);
|
||||
}
|
||||
}
|
||||
if (graph.StreamInstant) {
|
||||
// TODO(zaphar): Handle this?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryData} graph
|
||||
*/
|
||||
getLabelsForQueryData(graph) {
|
||||
const data = graph.plots;
|
||||
for (var subplot of data) {
|
||||
if (subplot.Series) {
|
||||
@ -379,15 +409,6 @@ export class GraphPlot extends HTMLElement {
|
||||
this.populateFilterData(labels);
|
||||
}
|
||||
}
|
||||
if (subplot.Stream) {
|
||||
for (const pair of subplot.Stream) {
|
||||
const labels = pair[0];
|
||||
this.populateFilterData(labels);
|
||||
}
|
||||
}
|
||||
if (subplot.StreamInstant) {
|
||||
// TODO(zaphar): Handle this?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -414,8 +435,8 @@ export class GraphPlot extends HTMLElement {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const meta = /** @type {PlotMeta} */(triple[1]);
|
||||
var yaxis = meta.yaxis || "y";
|
||||
const config = /** @type {PlotConfig} */(triple[1]);
|
||||
var yaxis = config.yaxis || "y";
|
||||
// https://plotly.com/javascript/reference/layout/yaxis/
|
||||
const series = triple[2];
|
||||
const trace = /** @type GraphTrace */({
|
||||
@ -428,10 +449,10 @@ export class GraphPlot extends HTMLElement {
|
||||
yaxis: yaxis,
|
||||
//yhoverformat: yaxis.tickformat,
|
||||
});
|
||||
if (meta.fill) {
|
||||
trace.fill = meta.fill;
|
||||
if (config.fill) {
|
||||
trace.fill = config.fill;
|
||||
}
|
||||
var name = this.formatName(meta, labels);
|
||||
var name = this.formatName(config, labels);
|
||||
if (name) { trace.name = name; }
|
||||
for (const point of series) {
|
||||
trace.x.push(new Date(point.timestamp * 1000));
|
||||
@ -451,15 +472,15 @@ export class GraphPlot extends HTMLElement {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const meta = /** @type {PlotMeta} */(triple[1]);
|
||||
const config = /** @type {PlotConfig} */(triple[1]);
|
||||
const series = triple[2];
|
||||
const trace = /** @type GraphTrace */({
|
||||
type: "bar",
|
||||
x: [],
|
||||
y: [],
|
||||
yhoverformat: meta["d3_tick_format"],
|
||||
yhoverformat: config["d3_tick_format"],
|
||||
});
|
||||
var name = this.formatName(meta, labels);
|
||||
var name = this.formatName(config, labels);
|
||||
if (name) { trace.name = name; }
|
||||
trace.y.push(series.value);
|
||||
trace.x.push(trace.name);
|
||||
@ -469,11 +490,11 @@ export class GraphPlot extends HTMLElement {
|
||||
/**
|
||||
* @param {Array} stream
|
||||
*
|
||||
* @returns {{dates: Array<string>, meta: Array<string>, lines: Array<string>}}
|
||||
* @returns {{dates: Array<string>, config: Array<string>, lines: Array<string>}}
|
||||
*/
|
||||
buildStreamPlot(stream) {
|
||||
const dateColumn = [];
|
||||
const metaColumn = [];
|
||||
const configColumn = [];
|
||||
const logColumn = [];
|
||||
|
||||
loopStream: for (const pair of stream) {
|
||||
@ -492,11 +513,11 @@ export class GraphPlot extends HTMLElement {
|
||||
// For streams the timestamps are in nanoseconds
|
||||
let timestamp = new Date(line.timestamp / 1000000);
|
||||
dateColumn.push(timestamp.toISOString());
|
||||
metaColumn.push(labelsName);
|
||||
configColumn.push(labelsName);
|
||||
logColumn.push(ansiToHtml(line.line));
|
||||
}
|
||||
}
|
||||
return { dates: dateColumn, meta: metaColumn, lines: logColumn };
|
||||
return { dates: dateColumn, config: configColumn, lines: logColumn };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -559,7 +580,7 @@ export class GraphPlot extends HTMLElement {
|
||||
});
|
||||
const columns = this.buildStreamPlot(logLineList.Stream);
|
||||
trace.cells.values.push(columns.dates);
|
||||
trace.cells.values.push(columns.meta);
|
||||
trace.cells.values.push(columns.config);
|
||||
trace.cells.values.push(columns.lines);
|
||||
traces.push(trace);
|
||||
} else if (logLineList.StreamInstant) {
|
454
tests/TAP.mjs
Normal file
454
tests/TAP.mjs
Normal file
@ -0,0 +1,454 @@
|
||||
/**
|
||||
* Tap - a 0 dependency TAP compliant test library useable from the commandline
|
||||
*
|
||||
* ```js
|
||||
* import { Tap } from './src/TAP.mjs';
|
||||
* var t = new Tap;
|
||||
* t.plan(3);
|
||||
*
|
||||
* t.ok(true, 'True is True'); # test will pass
|
||||
* t.is(1, 2, 'one is two'); # test will fail
|
||||
*
|
||||
* var obj = {};
|
||||
* obj.method1 = function() { return true; };
|
||||
*
|
||||
* t.can_ok(obj, 'method1'); # test will pass
|
||||
* ```
|
||||
*
|
||||
* @module TAP
|
||||
* @license Artistic-2.0
|
||||
*/
|
||||
|
||||
/** @implements TapRenderer */
|
||||
class NodeRenderer {
|
||||
/** @type {Array<PromiseLike>} */
|
||||
#thunks = [];
|
||||
|
||||
out(text) {
|
||||
this.#thunks.push(
|
||||
// Because this is a ECMAScript module we have to do dynamic module loads
|
||||
// of the node ecosystem when running in Node.js.
|
||||
import('node:process').then(loaded => {
|
||||
loaded.stdout.write(text + "\n");
|
||||
}));
|
||||
}
|
||||
|
||||
comment(lines) {
|
||||
for (var line of lines) {
|
||||
this.out('# ' + line);
|
||||
}
|
||||
}
|
||||
|
||||
// This gives us a way to block on output. It's ghetto but async is a harsh task master.
|
||||
async renderAll() {
|
||||
for (var thunk of this.#thunks) {
|
||||
await thunk;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/** @implements TapRenderer */
|
||||
class BrowserRenderer {
|
||||
#target = document.body;
|
||||
|
||||
/** @param {HtmlElement=} target */
|
||||
constructor(target) {
|
||||
if (target) {
|
||||
this.#target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns TextNode */
|
||||
#createText(text) {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} nodes
|
||||
* @returns HTMLDivElement
|
||||
*/
|
||||
#createDiv(nodes) {
|
||||
const div = document.createElement("div");
|
||||
div.append(...nodes);
|
||||
return div;
|
||||
}
|
||||
|
||||
out(text) {
|
||||
const textNode = this.#createText(text);
|
||||
var div = this.#createDiv([textNode]);
|
||||
if (text.startsWith("not ok")) {
|
||||
div.setAttribute("class", "fail");
|
||||
} else if (text.startsWith("ok")) {
|
||||
div.setAttribute("class", "pass");
|
||||
}
|
||||
this.#target.appendChild(div);
|
||||
}
|
||||
|
||||
comment(lines) {
|
||||
// TODO(jeremy):
|
||||
var elems = [];
|
||||
for (var line of lines) {
|
||||
elems.push(this.#createText("# " + line), document.createElement("br"));
|
||||
}
|
||||
var commentDiv = this.#createDiv(elems);
|
||||
commentDiv.setAttribute("class", "comment");
|
||||
this.#target.appendChild(commentDiv);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Tap Test Class helper.
|
||||
*/
|
||||
class Tap {
|
||||
/** @type Number? */
|
||||
planned = null;
|
||||
/** @type Number */
|
||||
counter = 0;
|
||||
/** @type Number */
|
||||
passed = 0;
|
||||
/** @type Number */
|
||||
failed = 0;
|
||||
/** @type TapRenderer */
|
||||
renderer
|
||||
|
||||
/**
|
||||
* Construct a new Tap Suite with a renderLine function.
|
||||
* @param {TapRenderer}
|
||||
*/
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{"Renderer": BrowserRenderer, "Tap": Tap}}
|
||||
*/
|
||||
static Browser() {
|
||||
var r = new BrowserRenderer();
|
||||
return {"Renderer": r, "Tap": new Tap(r)};
|
||||
return new Tap(new BrowserRenderer());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{"Renderer": NodeRenderer, "Tap": Tap}}
|
||||
*/
|
||||
static Node() {
|
||||
var r = new NodeRenderer();
|
||||
return {"Renderer": r, "Tap": new Tap(r)};
|
||||
}
|
||||
|
||||
isPass() {
|
||||
return this.passed != 0;
|
||||
}
|
||||
|
||||
/** Renders output for the test results */
|
||||
out(text) {
|
||||
this.renderer.out(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Tap output message.
|
||||
*
|
||||
* @param {boolean} ok
|
||||
* @param {string=} description
|
||||
*/
|
||||
mk_tap(ok, description) {
|
||||
if (!this.planned) {
|
||||
this.out("You tried to run tests without a plan. Gotta have a plan.");
|
||||
throw new Error("You tried to run tests without a plan. Gotta have a plan.");
|
||||
}
|
||||
this.counter++;
|
||||
this.out(ok + ' ' + this.counter + ' - ' + (description || ""));
|
||||
};
|
||||
|
||||
|
||||
comment(msg) {
|
||||
if (!msg) {
|
||||
msg = " ";
|
||||
}
|
||||
var lines = msg.split("\n");
|
||||
this.renderer.comment(lines);
|
||||
};
|
||||
|
||||
/** Render a pass TAP output message.
|
||||
* @param {string} description
|
||||
*/
|
||||
pass(description) {
|
||||
this.passed++;
|
||||
this.mk_tap('ok', description);
|
||||
};
|
||||
|
||||
/** Render a fail TAP output message.
|
||||
* @param {string} description
|
||||
*/
|
||||
fail(description) {
|
||||
this.failed++;
|
||||
this.mk_tap('not ok', description);
|
||||
};
|
||||
|
||||
/** Run a function as a TODO test.
|
||||
*
|
||||
* @param {function(this:Tap, boolean, description)} func
|
||||
*/
|
||||
todo(func) {
|
||||
var self = this;
|
||||
var tapper = self.mk_tap;
|
||||
self.mk_tap = function(ok, desc) {
|
||||
tapper.apply(self, [ok, "# TODO: " + desc]);
|
||||
}
|
||||
func();
|
||||
self.mk_tap = tapper;
|
||||
}
|
||||
|
||||
/** Run a function as a skip Test.
|
||||
*
|
||||
* @param {boolean} criteria
|
||||
* @param {string} reason
|
||||
* @param {number} count - The number of tests to skip
|
||||
* @param {function(this:Tap, boolean, description)} func
|
||||
*/
|
||||
skip(criteria, reason, count, func) {
|
||||
var self = this;
|
||||
if (criteria) {
|
||||
var tapper = self.mk_tap;
|
||||
self.mk_tap = function(ok, desc) {
|
||||
tapper.apply(self, [ok, desc]);
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
self.fail("# SKIP " + reason)
|
||||
}
|
||||
self.mk_tap = tapper;
|
||||
} else {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the test plan.
|
||||
* Once set this can not be reset again. Any attempt to change the plan once already
|
||||
* set will throw an exception.
|
||||
*
|
||||
* Call with no arguments if you don't want to specify the number of tests to run.
|
||||
*
|
||||
* @param {Number=} testCount
|
||||
*/
|
||||
plan(testCount) {
|
||||
if (this.planned) {
|
||||
throw new Error("you tried to set the plan twice!");
|
||||
}
|
||||
if (!testCount) {
|
||||
this.planned = 'no_plan';
|
||||
} else {
|
||||
this.planned = testCount;
|
||||
this.out('1..' + testCount);
|
||||
}
|
||||
};
|
||||
|
||||
#pass_if(func, desc) {
|
||||
var result = func();
|
||||
if (result) { this.pass(desc) }
|
||||
else { this.fail(desc) }
|
||||
}
|
||||
|
||||
// exception tests
|
||||
|
||||
/**
|
||||
* Tests that a function throws with a given error message.
|
||||
*
|
||||
* @param {function()} func
|
||||
* @param {RegExp} msg
|
||||
*/
|
||||
throws_ok(func, msg) {
|
||||
var errormsg = ' ';
|
||||
if (typeof func != 'function')
|
||||
this.comment('throws_ok needs a function to run');
|
||||
|
||||
try {
|
||||
func();
|
||||
}
|
||||
catch (err) {
|
||||
errormsg = err + '';
|
||||
}
|
||||
this.like(errormsg, msg, 'code threw [' + errormsg + '] expected: [' + msg + ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a function throws.
|
||||
*
|
||||
* @param {function()} func
|
||||
*/
|
||||
dies_ok(func) {
|
||||
var errormsg = ' ';
|
||||
var msg = false;
|
||||
if (typeof func != 'function')
|
||||
this.comment('throws_ok needs a function to run');
|
||||
|
||||
try {
|
||||
func();
|
||||
}
|
||||
catch (err) {
|
||||
errormsg = err + '';
|
||||
msg = true;
|
||||
}
|
||||
this.ok(msg, 'code died with [' + errormsg + ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a function does not throw an exception.
|
||||
*
|
||||
* @param {function()} func
|
||||
*/
|
||||
lives_ok(func, msg) {
|
||||
var errormsg = true;
|
||||
if (typeof func != 'function')
|
||||
this.comment('throws_ok needs a function to run');
|
||||
|
||||
try {
|
||||
func();
|
||||
}
|
||||
catch (err) {
|
||||
errormsg = false;
|
||||
}
|
||||
this.ok(errormsg, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that an object has a given method or function.
|
||||
*
|
||||
* @param {*} obj
|
||||
*/
|
||||
can_ok(obj) {
|
||||
var desc = 'object can [';
|
||||
var pass = true;
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
if (typeof (obj[arguments[i]]) != 'function') {
|
||||
if (typeof (obj.prototype) != 'undefined') {
|
||||
var result = typeof eval('obj.prototype.' + arguments[i]);
|
||||
if (result == 'undefined') {
|
||||
pass = false;
|
||||
this.comment('Missing ' + arguments[i] + ' method');
|
||||
}
|
||||
} else {
|
||||
pass = false;
|
||||
this.comment('Missing ' + arguments[i] + ' method');
|
||||
}
|
||||
}
|
||||
desc += ' ' + arguments[i];
|
||||
}
|
||||
desc += ' ]';
|
||||
this.#pass_if(function() {
|
||||
return pass;
|
||||
}, desc);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that two given objects are equal.
|
||||
*
|
||||
* @param {*} got
|
||||
* @param {*} expected
|
||||
* @param {string} desc
|
||||
*/
|
||||
is(got, expected, desc) {
|
||||
this.#pass_if(function() { return got == expected; }, desc);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test that expression evaluates to true value
|
||||
*
|
||||
* @param {*} got
|
||||
* @param {string} desc
|
||||
*/
|
||||
ok(got, desc) {
|
||||
this.#pass_if(function() { return !!got; }, desc);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests that a string matches the regex.
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {RegExp} regex
|
||||
* @param {string} desc
|
||||
*/
|
||||
like(string, regex, desc) {
|
||||
this.#pass_if(function() {
|
||||
if (regex instanceof RegExp) {
|
||||
return string.match(regex)
|
||||
} else {
|
||||
return string.indexOf(regex) != -1
|
||||
}
|
||||
}, desc)
|
||||
}
|
||||
|
||||
/**
|
||||
* The opposite of like. tests that the string doesn't match the regex.
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {RegExp} regex
|
||||
* @param {string} desc
|
||||
*/
|
||||
unlike(string, regex, desc) {
|
||||
this.#pass_if(function() {
|
||||
return !string.match(regex);
|
||||
}, desc)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a test and render a summarization.
|
||||
*
|
||||
* @param {Tap} t
|
||||
* @param {string} testName
|
||||
* @param {function(Tap)} test
|
||||
*/
|
||||
function runTest(t, testName, test) {
|
||||
t.comment('running ' + testName + ' tests');
|
||||
try {
|
||||
test(t);
|
||||
summarize(t);
|
||||
}
|
||||
catch (err) {
|
||||
t.comment("Test Suite Crashed!!! (" + err + ")");
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a summary of the tests as comments.
|
||||
*
|
||||
* @param {Tap} t
|
||||
*/
|
||||
function summarize(t) {
|
||||
if (t.planned > t.counter) {
|
||||
t.comment('looks like you planned ' + t.planned + ' tests but only ran '
|
||||
+ t.counter + ' tests');
|
||||
} else if (t.planned < t.counter) {
|
||||
t.comment('looks like you planned ' + t.planned + ' tests but ran '
|
||||
+ (t.counter - t.planned) + ' tests extra');
|
||||
}
|
||||
t.comment('ran ' + t.counter + ' tests out of ' + t.planned);
|
||||
t.comment('passed ' + t.passed + ' tests out of ' + t.planned);
|
||||
t.comment('failed ' + t.failed + ' tests out of ' + t.planned);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tap} t
|
||||
* @param {Array<{'plan': Number, name: string, 'test': function(Tap)}} suite
|
||||
*/
|
||||
function runSuite(t, suite) {
|
||||
const plan = suite.reduce((acc, item) => {
|
||||
return acc + item.plan
|
||||
}, 0);
|
||||
t.plan(plan);
|
||||
for (var item of suite) {
|
||||
t.comment('running ' + item.name + ' tests');
|
||||
item.test(t);
|
||||
}
|
||||
summarize(t);
|
||||
}
|
||||
|
||||
export { Tap, runTest, runSuite, summarize, BrowserRenderer, NodeRenderer };
|
||||
|
20
tests/Tap.d.js
Normal file
20
tests/Tap.d.js
Normal file
@ -0,0 +1,20 @@
|
||||
/** @interface */
|
||||
class TapRenderer {
|
||||
/** Renders output for the test results
|
||||
* @param {string} text
|
||||
*/
|
||||
out(text) {
|
||||
}
|
||||
|
||||
/** Diagnostic formatter for TAP Output.
|
||||
*
|
||||
* @param {Array<string>} lines
|
||||
*/
|
||||
comment(lines) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef TapSuite
|
||||
* @type {Array<{'plan': Number, name: string, 'test': function(Tap)}}
|
||||
*/
|
44
tests/TapHarness.html
Normal file
44
tests/TapHarness.html
Normal file
@ -0,0 +1,44 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<style type="text/css">
|
||||
.test {
|
||||
margin-top : 10px;
|
||||
margin-bottom : 10px;
|
||||
border : 3px;
|
||||
border-style : inset;
|
||||
overflow : auto;
|
||||
}
|
||||
|
||||
.small {
|
||||
height : 20px;
|
||||
}
|
||||
|
||||
.big {
|
||||
height : 600px;
|
||||
}
|
||||
|
||||
.comment { color: darkgray; }
|
||||
.pass { color: green; }
|
||||
.fail { color: red; }
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
/** Configuration options
|
||||
*
|
||||
*/
|
||||
var tests = [
|
||||
'01_tap.t.js',
|
||||
];
|
||||
</script>
|
||||
Heracles custom elements test harness.
|
||||
<script type="module">
|
||||
import { Tap, runSuite } from './TAP.mjs';
|
||||
import { tapSuite } from './suite.mjs';
|
||||
|
||||
const pair = Tap.Browser();
|
||||
runSuite(pair.Tap, tapSuite);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
57
tests/suite.mjs
Normal file
57
tests/suite.mjs
Normal file
@ -0,0 +1,57 @@
|
||||
// TODO(jwall): Figure out how to handle the missing browser apis in node contexts.
|
||||
import { GraphPlot, SpanSelector } from '../static/lib.mjs';
|
||||
|
||||
function deepEqual(got, expected) {
|
||||
// Check if both are the same reference or both are null
|
||||
if (got === expected) { return true; }
|
||||
|
||||
// Check if both are objects (including arrays) and neither is null
|
||||
if (typeof got !== 'object' || got === null ||
|
||||
typeof expected !== 'object' || expected === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the keys of both objects
|
||||
const keysGot = Object.keys(got), keysExpected = Object.keys(expected);
|
||||
|
||||
// If number of properties is different, objects are not equivalent
|
||||
if (keysGot.length !== keysExpected.length) { return false; }
|
||||
|
||||
// Check all properties of a are in b and are equal
|
||||
for (let key of keysGot) {
|
||||
if (!keysExpected.includes(key) || !deepEqual(got[key], expected[key])) { return false; }
|
||||
}
|
||||
|
||||
// If we got this far, objects are considered equivalent
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {TapSuite}
|
||||
*/
|
||||
export var tapSuite = [
|
||||
{
|
||||
plan: 2,
|
||||
name: "Custom Element registration Tests",
|
||||
test: function(t) {
|
||||
t.ok(customElements.get(GraphPlot.elementName), `GraphPlot element is registered with name ${GraphPlot.elementName}`);
|
||||
t.ok(customElements.get(SpanSelector.elementName), `SpanSelector element is registered with name ${SpanSelector.elementName}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
plan: 5,
|
||||
name: "PopulateFilterData test",
|
||||
test: function(t) {
|
||||
const plot = new GraphPlot();
|
||||
t.ok(deepEqual(plot.getFilterLabels(), {}), "filter lables start out empty");
|
||||
plot.populateFilterData({});
|
||||
t.ok(typeof(deepEqual(plot.getFilterLabels()), {}), "filter lables are still empty");
|
||||
plot.populateFilterData({"foo": "bar"});
|
||||
t.ok(deepEqual(plot.getFilterLabels(), {"foo": ["bar"]}), "filter labels get set with list of one item on first label");
|
||||
plot.populateFilterData({"foo": "quux"});
|
||||
t.ok(deepEqual(plot.getFilterLabels(), {"foo": ["bar", "quux"]}), "list of two values after second label value");
|
||||
plot.populateFilterData({"foo": "bar"});
|
||||
t.ok(deepEqual(plot.getFilterLabels(), {"foo": ["bar", "quux"]}), "We don't double add the same value");
|
||||
}
|
||||
}
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user