Skip to content

Commit 9102c1b

Browse files
danceratopzmarioevz
authored andcommitted
cmd/hiveview: Update UI to support line ranges in shared client logs
Updates the UI to properly display log links for shared clients with line range highlighting. Enhances client detection logic to work with multiple client instances of the same type, and improves the log viewer to properly highlight line ranges.
1 parent fbae8b0 commit 9102c1b

File tree

4 files changed

+290
-40
lines changed

4 files changed

+290
-40
lines changed

cmd/hiveview/assets/lib/app-suite.js

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -317,21 +317,175 @@ function deselectTest(row, closeDetails) {
317317
history.replaceState(null, null, '#');
318318
}
319319

320-
function testHasClients(testData) {
321-
return testData.clientInfo && Object.getOwnPropertyNames(testData.clientInfo).length > 0;
320+
function testHasClients(testData, suiteData) {
321+
if (testData.clientInfo && Object.getOwnPropertyNames(testData.clientInfo).length > 0) {
322+
return true;
323+
}
324+
325+
if (testData.summaryResult && testData.summaryResult.clientLogs &&
326+
Object.keys(testData.summaryResult.clientLogs).length > 0) {
327+
return true;
328+
}
329+
330+
if (suiteData && suiteData.sharedClients) {
331+
for (let clientID in suiteData.sharedClients) {
332+
let clientName = suiteData.sharedClients[clientID].name;
333+
if (testData.name.includes(clientName)) {
334+
return true;
335+
}
336+
}
337+
}
338+
339+
return false;
322340
}
323341

324342
// formatClientLogsList turns the clientInfo part of a test into a list of links.
325343
function formatClientLogsList(suiteData, testIndex, clientInfo) {
326344
let links = [];
327-
for (let instanceID in clientInfo) {
328-
let instanceInfo = clientInfo[instanceID];
329-
let logfile = routes.resultsRoot + instanceInfo.logFile;
330-
let url = routes.clientLog(suiteData.suiteID, suiteData.name, testIndex, logfile);
331-
let link = html.makeLink(url, instanceInfo.name);
332-
link.classList.add('log-link');
333-
links.push(link.outerHTML);
345+
let testCase = suiteData.testCases[testIndex];
346+
let usedSharedClients = new Set(); // Track which shared clients were used in this test
347+
348+
// First, check if the test has specific information about which shared clients it used
349+
if (testCase && testCase.summaryResult && testCase.summaryResult.clientLogs) {
350+
for (let clientID in testCase.summaryResult.clientLogs) {
351+
usedSharedClients.add(clientID);
352+
}
353+
}
354+
355+
// Handle clients listed directly in the test's clientInfo
356+
if (clientInfo) {
357+
for (let instanceID in clientInfo) {
358+
let instanceInfo = clientInfo[instanceID];
359+
360+
// Skip if no log file
361+
if (!instanceInfo.logFile) {
362+
continue;
363+
}
364+
365+
// If it's a shared client, mark it as used
366+
if (instanceInfo.isShared) {
367+
usedSharedClients.add(instanceID);
368+
}
369+
370+
let logfile = routes.resultsRoot + instanceInfo.logFile;
371+
let url = routes.clientLog(suiteData.suiteID, suiteData.name, testIndex, logfile);
372+
373+
// Check if this is a shared client with a log segment
374+
let hasSegment = testCase &&
375+
testCase.summaryResult &&
376+
testCase.summaryResult.clientLogs &&
377+
testCase.summaryResult.clientLogs[instanceID];
378+
379+
if (hasSegment) {
380+
// If we have a log segment, update the URL to include the line numbers
381+
const clientLogInfo = testCase.summaryResult.clientLogs[instanceID];
382+
383+
// Use line numbers from the backend
384+
url += `#L${clientLogInfo.startLine}-${clientLogInfo.endLine}`;
385+
}
386+
387+
// Add "(shared)" indicator for shared clients
388+
let clientName = instanceInfo.name;
389+
if (instanceInfo.isShared || hasSegment) {
390+
clientName += " (shared)";
391+
}
392+
393+
let link = html.makeLink(url, clientName);
394+
link.classList.add('log-link');
395+
if (instanceInfo.isShared) {
396+
link.classList.add('shared-client-log');
397+
}
398+
links.push(link.outerHTML);
399+
}
400+
}
401+
402+
// For backward compatibility - if test name includes client name, add that client
403+
// This handles the case where tests don't yet have clientInfo or clientLogs properly populated
404+
if (suiteData.sharedClients) {
405+
406+
// First try to match by existing client logs
407+
if (usedSharedClients.size === 0) {
408+
// Group clients by name to identify if there are multiple of the same type
409+
let clientsByName = {};
410+
for (let instanceID in suiteData.sharedClients) {
411+
let sharedClient = suiteData.sharedClients[instanceID];
412+
if (!sharedClient.logFile) continue; // Skip if no log file
413+
414+
// Add to the clients by name map
415+
if (!clientsByName[sharedClient.name]) {
416+
clientsByName[sharedClient.name] = [];
417+
}
418+
clientsByName[sharedClient.name].push({id: instanceID, client: sharedClient});
419+
}
420+
421+
// Now check test name for client match, but only if there's exactly one client of that type
422+
for (let clientName in clientsByName) {
423+
if (testCase.name.includes(clientName) && clientsByName[clientName].length === 1) {
424+
// If there's exactly one client of this type, it's safe to auto-register
425+
let instanceID = clientsByName[clientName][0].id;
426+
usedSharedClients.add(instanceID);
427+
}
428+
}
429+
}
430+
431+
// Now add all the used shared clients that haven't been handled yet
432+
for (let instanceID in suiteData.sharedClients) {
433+
// First check if this client is explicitly registered in the test's clientLogs
434+
// This is the most reliable way to determine if a client was used in a test
435+
const explicitlyRegistered = testCase &&
436+
testCase.summaryResult &&
437+
testCase.summaryResult.clientLogs &&
438+
testCase.summaryResult.clientLogs[instanceID];
439+
440+
if (explicitlyRegistered) {
441+
usedSharedClients.add(instanceID);
442+
}
443+
444+
// Skip if not used by this test (based on explicit tracking or name matching)
445+
if (!usedSharedClients.has(instanceID)) {
446+
continue;
447+
}
448+
449+
// Skip clients already handled in clientInfo
450+
if (clientInfo && instanceID in clientInfo) {
451+
continue;
452+
}
453+
454+
let sharedClient = suiteData.sharedClients[instanceID];
455+
456+
// Skip if no log file
457+
if (!sharedClient.logFile) {
458+
continue;
459+
}
460+
461+
// Create a link to the full log file for this shared client
462+
let logfile = routes.resultsRoot + sharedClient.logFile;
463+
let url = routes.clientLog(suiteData.suiteID, suiteData.name, testIndex, logfile);
464+
465+
// Check if we have specific log segments for this client in this test
466+
let hasSegment = testCase &&
467+
testCase.summaryResult &&
468+
testCase.summaryResult.clientLogs &&
469+
testCase.summaryResult.clientLogs[instanceID];
470+
471+
if (hasSegment) {
472+
// If we have a log segment, update the URL to include the line numbers
473+
const clientLogInfo = testCase.summaryResult.clientLogs[instanceID];
474+
475+
// Only add line range if we have valid line numbers (both > 0)
476+
if (clientLogInfo.startLine > 0 && clientLogInfo.endLine > 0) {
477+
url += `#L${clientLogInfo.startLine}-${clientLogInfo.endLine}`;
478+
}
479+
}
480+
481+
let clientName = sharedClient.name + " (shared)";
482+
let link = html.makeLink(url, clientName);
483+
484+
link.classList.add('log-link', 'shared-client-log');
485+
links.push(link.outerHTML);
486+
}
334487
}
488+
335489
return links.join(', ');
336490
}
337491

@@ -360,7 +514,7 @@ function formatTestDetails(suiteData, row) {
360514
p.innerHTML = formatTestStatus(d.summaryResult);
361515
container.appendChild(p);
362516
}
363-
if (!row.column('logs:name').responsiveHidden() && testHasClients(d)) {
517+
if (!row.column('logs:name').responsiveHidden() && testHasClients(d, suiteData)) {
364518
let p = document.createElement('p');
365519
p.innerHTML = '<b>Clients:</b> ' + formatClientLogsList(suiteData, d.testIndex, d.clientInfo);
366520
container.appendChild(p);

cmd/hiveview/assets/lib/app-viewer.js

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ import { formatBytes, queryParam } from './utils.js';
1212
$(document).ready(function () {
1313
common.updateHeader();
1414

15-
// Check for line number in hash.
16-
var line = null;
17-
if (window.location.hash.substr(1, 1) == 'L') {
18-
line = parseInt(window.location.hash.substr(2));
15+
// Check for line number or line range in hash.
16+
var startLine = null;
17+
var endLine = null;
18+
var hash = window.location.hash.substr(1);
19+
if (hash.startsWith('L')) {
20+
var range = hash.substr(1).split('-');
21+
startLine = parseInt(range[0]);
22+
if (range.length > 1) {
23+
endLine = parseInt(range[1]);
24+
}
1925
}
2026

2127
// Get suite context.
@@ -33,7 +39,7 @@ $(document).ready(function () {
3339
showError('Invalid parameters! Missing \'suitefile\' or \'testid\' in URL.');
3440
return;
3541
}
36-
fetchTestLog(routes.resultsRoot + suiteFile, testIndex, line);
42+
fetchTestLog(routes.resultsRoot + suiteFile, testIndex, startLine, endLine);
3743
return;
3844
}
3945

@@ -42,35 +48,61 @@ $(document).ready(function () {
4248
if (file) {
4349
$('#fileload').val(file);
4450
showText('Loading file...');
45-
fetchFile(file, line);
51+
fetchFile(file, startLine, endLine);
4652
return;
4753
}
4854

4955
// Show default text because nothing was loaded.
5056
showText(document.getElementById('exampletext').innerHTML);
5157
});
5258

53-
// setHL sets the highlight on a line number.
54-
function setHL(num, scroll) {
59+
// setHL sets the highlight on a line number or range of lines.
60+
function setHL(startLine, endLine, scroll) {
5561
// out with the old
5662
$('.highlighted').removeClass('highlighted');
57-
if (!num) {
63+
if (!startLine) {
5864
return;
5965
}
6066

6167
let contentArea = document.getElementById('file-content');
6268
let gutter = document.getElementById('gutter');
63-
let numElem = gutter.children[num - 1];
64-
if (!numElem) {
65-
console.error('invalid line number:', num);
66-
return;
69+
70+
// Calculate the end line if not provided
71+
if (!endLine) {
72+
endLine = startLine;
73+
}
74+
75+
// Calculate max available lines and adjust range if needed
76+
const maxLines = gutter.children.length;
77+
78+
// Check if the requested range is beyond the file size
79+
if (startLine > maxLines) {
80+
startLine = 1;
81+
}
82+
83+
if (endLine > maxLines) {
84+
endLine = maxLines;
6785
}
68-
// in with the new
69-
let lineElem = contentArea.children[num - 1];
70-
$(numElem).addClass('highlighted');
71-
$(lineElem).addClass('highlighted');
86+
87+
// Highlight all lines in the adjusted range
88+
for (let num = startLine; num <= endLine; num++) {
89+
let numElem = gutter.children[num - 1];
90+
if (!numElem) {
91+
// Skip invalid line numbers
92+
continue;
93+
}
94+
95+
let lineElem = contentArea.children[num - 1];
96+
$(numElem).addClass('highlighted');
97+
$(lineElem).addClass('highlighted');
98+
}
99+
100+
// Scroll to the start of the highlighted range
72101
if (scroll) {
73-
numElem.scrollIntoView();
102+
let firstNumElem = gutter.children[startLine - 1];
103+
if (firstNumElem) {
104+
firstNumElem.scrollIntoView();
105+
}
74106
}
75107
}
76108

@@ -163,12 +195,12 @@ function appendLine(contentArea, gutter, number, text) {
163195
}
164196

165197
function lineNumberClicked() {
166-
setHL($(this).attr('line'), false);
198+
setHL(parseInt($(this).attr('line')), null, false);
167199
history.replaceState(null, null, '#' + $(this).attr('id'));
168200
}
169201

170202
// fetchFile loads up a new file to view
171-
async function fetchFile(url, line /* optional jump to line */ ) {
203+
async function fetchFile(url, startLine, endLine) {
172204
let resultsRE = new RegExp('^' + routes.resultsRoot);
173205
let text;
174206
try {
@@ -181,11 +213,11 @@ async function fetchFile(url, line /* optional jump to line */ ) {
181213
let title = url.replace(resultsRE, '');
182214
showTitle(null, title);
183215
showText(text);
184-
setHL(line, true);
216+
setHL(startLine, endLine, true);
185217
}
186218

187219
// fetchTestLog loads the suite file and displays the output of a test.
188-
async function fetchTestLog(suiteFile, testIndex, line) {
220+
async function fetchTestLog(suiteFile, testIndex, startLine, endLine) {
189221
let data;
190222
try {
191223
data = await load(suiteFile, 'json');
@@ -221,7 +253,7 @@ async function fetchTestLog(suiteFile, testIndex, line) {
221253
}
222254
showTitle('Test:', name);
223255
showText(logtext);
224-
setHL(line, true);
256+
setHL(startLine, endLine, true);
225257
}
226258

227259
async function load(url, dataType) {

hivesim/hive.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,33 @@ func (sim *Simulation) ExecSharedClient(testSuite SuiteID, clientID string, cmd
284284
return resp, err
285285
}
286286

287+
// RegisterNode registers a client with a test. This is normally handled
288+
// automatically by StartClient, but can be used directly to register a reference
289+
// to a shared client.
290+
func (sim *Simulation) RegisterNode(testSuite SuiteID, test TestID, clientID string, nodeInfo *simapi.NodeInfo) error {
291+
if sim.docs != nil {
292+
return errors.New("RegisterNode is not supported in docs mode")
293+
}
294+
295+
// We'll use the startClient endpoint with a special parameter to register a shared client
296+
var (
297+
url = fmt.Sprintf("%s/testsuite/%d/test/%d/node", sim.url, testSuite, test)
298+
config = simapi.NodeConfig{
299+
Client: nodeInfo.Name,
300+
SharedClientID: clientID,
301+
}
302+
)
303+
304+
// Set up a client setup object to post with files (even though we don't have any files)
305+
setup := &clientSetup{
306+
files: make(map[string]func() (io.ReadCloser, error)),
307+
config: config,
308+
}
309+
310+
var resp simapi.StartNodeResponse
311+
return setup.postWithFiles(url, &resp)
312+
}
313+
287314
// StopClient signals to the host that the node is no longer required.
288315
func (sim *Simulation) StopClient(testSuite SuiteID, test TestID, nodeid string) error {
289316
if sim.docs != nil {

0 commit comments

Comments
 (0)