ce29202faf7a2b48e62a6e3f579ba09000d00c0b braney Sun Sep 27 15:51:36 2026 -0700 docent: put the server's warning text in a failure message, refs #38252 A run that failed on noText: "Warning/Error", or on a custom track that never loaded, said only that the server complained. The reason was left in the Apache error log. The new serverWarnings() reads the hgTracks warning list and hgCustom's Error/Warning line. A failed expect: and a failed addCustomTrack now print each one as a "server said:" line. diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js index 7659a4ba4ed..386c77a524f 100755 --- src/hg/utils/docent/docent.js +++ src/hg/utils/docent/docent.js @@ -1551,30 +1551,56 @@ + `${new URL(SERVER).host}, so it is loaded there instead ` + `-- a named session has to exist on the machine being driven`); if (share) await nav(`/cgi-bin/hgTracks?hgS_doOtherUser=submit` + `&hgS_otherUserName=${enc(decodeURIComponent(share[1]))}` + `&hgS_otherUserSessionName=${enc(decodeURIComponent(share[2]))}`); else if (hgs) await nav(`/cgi-bin/hgTracks?${u.search.slice(1)}`); else await nav(`/cgi-bin/hgTracks?hgS_doLoadUrl=submit&hgS_loadUrlName=${enc(String(from))}`); } await page.waitForSelector('#imgTbl').catch(() => {}); await captureState(); if (o.shot) await shot(o.shot); } + // The warning text the server put on the page, for a failure message. hgTracks lists each + // warn() as an <li> under #warnList. hgCustom prints a red "Error" (orange "Warning") span + // and the message after it. Without this a run that fails on noText: "Warning/Error", or + // on a custom track that never loaded, says only THAT the server complained, and the why + // is left in the Apache error log. The 2026-09-27 nightly lost two scripts that way to a + // 20-minute hgdownload outage, and the log alone could not tell it from a regression. + async function serverWarnings() { + const said = await page.evaluate(() => { + const out = [...document.querySelectorAll('#warnList li')].map(e => e.textContent); + // hgCustom writes <B><span>Error</span><P>message</B>, and the parser moves the + // message out of the span's element, so take the page text that follows the span. + const body = document.body ? document.body.innerText : ''; + for (const s of document.querySelectorAll('span')) { + const t = s.textContent.trim(); + if ((t !== 'Error' && t !== 'Warning') || !/red|orange/i.test(s.style.color)) continue; + const r = document.createRange(); + r.setStartAfter(s); + r.setEnd(document.body, document.body.childNodes.length); + const after = r.toString().trim().split('\n')[0]; + if (after) out.push(`${t}: ${after}`); + } + return out; + }).catch(() => []); + const clip = s => s.replace(/\s+/g, ' ').trim().slice(0, 400); + return [...new Set(said.map(clip).filter(Boolean))].slice(0, 5); + } // The only verb that can fail a run. Every other verb renders happily whatever it is // handed: a superTrack that came up whole and made an image 7,581 px tall, a subtrack // that never hid, a pinned tooltip that grabbed the neighbouring item, an Apache 414 // page where the view should be. All of those shipped once and all were caught by eye. // Stating the expectation instead stops the run, non-zero, at the step that broke it -- // `make` then fails rather than writing a wrong figure over a right one. // // expect: {rows: [ruler, mane]} these rows were drawn // expect: {rows: [ruler, mane], exact: true} ... and nothing else // expect: {rows: [ruler, mane], ordered: true} ... in that order, top to bottom // expect: {noRows: [clinvarCnv]} this row was not // expect: {height: 2000} the still is no taller than this ("<1200" etc.) // expect: {tip: "mismatch A->C"} the tooltip now up says this // expect: {text: "...", noText: "..."} the page does / does not contain this // expect: {url: "hgSearch", noUrl: "%E2%80%8B"} the address bar does / does not @@ -1760,31 +1786,32 @@ + ` -- the row holds ${show(got)}`); if (k === 'not' && near(dom, rgb)) bad.push(`${where} is drawn ${dom.join(',')}, which is the color it should not be` + ` -- the row holds ${show(got)}`); } if (process.env.DOCENT_ROWS) console.log(` color ${where}: ${show(got)}` + ` [x ${got.box[0]}-${got.box[1]} of ${got.box[2]}, ${got.box[3]}px tall]`); } } } if (!bad.length) { console.log(`EXPECT ok -- ${seen.rows.length} row(s), ${height}px`); return; } - const msg = bad.join('; ') + `\n drawn: ${seen.rows.join(', ') || '(none)'}`; + const said = (await serverWarnings()).map(w => `\n server said: ${w}`).join(''); + const msg = bad.join('; ') + `\n drawn: ${seen.rows.join(', ') || '(none)'}` + said; if (o.warn) console.warn('EXPECT (warning only):', msg); else throw new Error(msg); } // Shift+drag across the track image to open the browser's own drag-select dialog // ("Zoom In / Single Highlight / ..."), then act on it. The usual form gives one // genomic region and zooms: drag: chr7:155,806,100-155,806,557 // Any other action needs the map form, which is also how you pass shot:/track: // drag: {range: "chr7:155,806,100-155,806,557", then: highlight} // Endpoints that are not genomic coords use a fraction // across the view (fromFrac:/toFrac:) or a raw pixel (fromX:/toX:) instead. // Optional `track:` picks the row the drag runs over (y); default is the middle of // the image. `shot:` captures the open dialog (e.g. the Figure 1A drag-select box). // `then:` = zoom (default, clicks Zoom In) | highlight (Single Highlight) | cancel // (Escape, leaves the view unchanged) | none (leaves the dialog OPEN, so later steps can // tick its checkbox or press one of its other buttons with click:). @@ -2424,30 +2451,31 @@ const s = String(data); if (s.length <= 400) { for (const ch of s) { await page.keyboard.insertText(ch); await sleep(12); } } else { await page.keyboard.insertText(s); } await sleep(300); await clickGlide('#Submit'); await page.waitForLoadState('load'); // The manage page appears on success; click through to the browser (a data error // re-shows the add page instead, so guard on the button being present). const goSel = (o.goto === 'current') ? '#submitGoBack' : '#submit'; if (await page.locator(goSel).count()) { await clickGlide(goSel); await page.waitForSelector('#imgTbl').catch(() => {}); await captureState(); } else { console.warn('addCustomTrack: submit did not reach the manage page (data error?)'); + for (const w of await serverWarnings()) console.warn(` server said: ${w}`); } if (o.shot) { await shot(o.shot); return; } break; } case 'hubUpload': { // Add files to the Hub Upload dashboard (hgHubConnect -> Hub Upload -> Upload), the // way a user dropping them there would. Takes {name:, text:} for one file, or // {files: [{name:, text:}, ...]} for several, and shot:. The files in one step arrive // together, as one batch, which is what the dashboard does with a multi-file drop. // Nothing is uploaded: the files wait in the dashboard, so a run leaves nothing on the // server. The page needs a user, so a login: step comes first. const o = (arg && typeof arg === 'object') ? arg : {}; const files = o.files || (o.name != null ? [{ name: o.name, text: o.text }] : []); if (!files.length) throw new Error('hubUpload: takes {name:, text:} or {files: [{name:, text:}, ...]}');