github.com/thanos-io/thanos@v0.32.5/pkg/ui/react-app/src/utils/utils.test.ts (about) 1 import moment from 'moment'; 2 3 import { 4 decodePanelOptionsFromQueryString, 5 encodePanelOptionsToQueryString, 6 escapeHTML, 7 formatDuration, 8 formatTime, 9 formatRelative, 10 humanizeDuration, 11 metricToSeriesName, 12 now, 13 parseDuration, 14 parseOption, 15 parseTime, 16 toQueryString, 17 } from '.'; 18 import { PanelType } from '../pages/graph/Panel'; 19 20 describe('Utils', () => { 21 describe('escapeHTML', (): void => { 22 it('escapes html sequences', () => { 23 expect(escapeHTML(`<strong>'example'&"another/example"</strong>`)).toEqual( 24 '<strong>'example'&"another/example"</strong>' 25 ); 26 }); 27 }); 28 29 describe('metricToSeriesName', () => { 30 it('returns "{}" if labels is empty', () => { 31 const labels = {}; 32 expect(metricToSeriesName(labels)).toEqual('{}'); 33 }); 34 it('returns "metric_name{}" if labels only contains __name__', () => { 35 const labels = { __name__: 'metric_name' }; 36 expect(metricToSeriesName(labels)).toEqual('metric_name{}'); 37 }); 38 it('returns "{label1=value_1, ..., labeln=value_n} if there are many labels and no name', () => { 39 const labels = { label1: 'value_1', label2: 'value_2', label3: 'value_3' }; 40 expect(metricToSeriesName(labels)).toEqual('{label1="value_1", label2="value_2", label3="value_3"}'); 41 }); 42 it('returns "metric_name{label1=value_1, ... ,labeln=value_n}" if there are many labels and a name', () => { 43 const labels = { 44 __name__: 'metric_name', 45 label1: 'value_1', 46 label2: 'value_2', 47 label3: 'value_3', 48 }; 49 expect(metricToSeriesName(labels)).toEqual('metric_name{label1="value_1", label2="value_2", label3="value_3"}'); 50 }); 51 }); 52 53 describe('Time format', () => { 54 describe('formatTime', () => { 55 it('returns a time string representing the time in seconds', () => { 56 expect(formatTime(1572049380000)).toEqual('2019-10-26 00:23:00'); 57 expect(formatTime(0)).toEqual('1970-01-01 00:00:00'); 58 }); 59 }); 60 61 describe('parseTime', () => { 62 it('returns a time string representing the time in seconds', () => { 63 expect(parseTime('2019-10-26 00:23')).toEqual(1572049380000); 64 expect(parseTime('1970-01-01 00:00')).toEqual(0); 65 expect(parseTime('0001-01-01T00:00:00Z')).toEqual(-62135596800000); 66 }); 67 }); 68 69 describe('parseDuration and formatDuration', () => { 70 describe('should parse and format durations correctly', () => { 71 const tests: { input: string; output: number; expectedString?: string }[] = [ 72 { 73 input: '0', 74 output: 0, 75 expectedString: '0s', 76 }, 77 { 78 input: '0w', 79 output: 0, 80 expectedString: '0s', 81 }, 82 { 83 input: '0s', 84 output: 0, 85 }, 86 { 87 input: '324ms', 88 output: 324, 89 }, 90 { 91 input: '3s', 92 output: 3 * 1000, 93 }, 94 { 95 input: '5m', 96 output: 5 * 60 * 1000, 97 }, 98 { 99 input: '1h', 100 output: 60 * 60 * 1000, 101 }, 102 { 103 input: '4d', 104 output: 4 * 24 * 60 * 60 * 1000, 105 }, 106 { 107 input: '4d1h', 108 output: 4 * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000, 109 }, 110 { 111 input: '14d', 112 output: 14 * 24 * 60 * 60 * 1000, 113 expectedString: '2w', 114 }, 115 { 116 input: '3w', 117 output: 3 * 7 * 24 * 60 * 60 * 1000, 118 }, 119 { 120 input: '3w2d1h', 121 output: 3 * 7 * 24 * 60 * 60 * 1000 + 2 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000, 122 expectedString: '23d1h', 123 }, 124 { 125 input: '1y2w3d4h5m6s7ms', 126 output: 127 1 * 365 * 24 * 60 * 60 * 1000 + 128 2 * 7 * 24 * 60 * 60 * 1000 + 129 3 * 24 * 60 * 60 * 1000 + 130 4 * 60 * 60 * 1000 + 131 5 * 60 * 1000 + 132 6 * 1000 + 133 7, 134 expectedString: '382d4h5m6s7ms', 135 }, 136 ]; 137 138 tests.forEach((t) => { 139 it(t.input, () => { 140 const d = parseDuration(t.input); 141 expect(d).toEqual(t.output); 142 expect(formatDuration(d!)).toEqual(t.expectedString || t.input); 143 }); 144 }); 145 }); 146 147 describe('should fail to parse invalid durations', () => { 148 const tests = ['1', '1y1m1d', '-1w', '1.5d', 'd', '']; 149 150 tests.forEach((t) => { 151 it(t, () => { 152 expect(parseDuration(t)).toBe(null); 153 }); 154 }); 155 }); 156 }); 157 158 describe('humanizeDuration', () => { 159 it('humanizes zero', () => { 160 expect(humanizeDuration(0)).toEqual('0s'); 161 }); 162 it('humanizes milliseconds', () => { 163 expect(humanizeDuration(1.234567)).toEqual('1.235ms'); 164 expect(humanizeDuration(12.34567)).toEqual('12.346ms'); 165 expect(humanizeDuration(123.45678)).toEqual('123.457ms'); 166 expect(humanizeDuration(123)).toEqual('123.000ms'); 167 }); 168 it('humanizes seconds', () => { 169 expect(humanizeDuration(12340)).toEqual('12.340s'); 170 }); 171 it('humanizes minutes', () => { 172 expect(humanizeDuration(1234567)).toEqual('20m 34s'); 173 }); 174 175 it('humanizes hours', () => { 176 expect(humanizeDuration(12345678)).toEqual('3h 25m 45s'); 177 }); 178 179 it('humanizes days', () => { 180 expect(humanizeDuration(123456789)).toEqual('1d 10h 17m 36s'); 181 expect(humanizeDuration(123456789000)).toEqual('1428d 21h 33m 9s'); 182 }); 183 it('takes sign into account', () => { 184 expect(humanizeDuration(-123456789000)).toEqual('-1428d 21h 33m 9s'); 185 }); 186 }); 187 188 describe('formatRelative', () => { 189 it('renders never for pre-beginning-of-time strings', () => { 190 expect(formatRelative('0001-01-01T00:00:00Z', now())).toEqual('Never'); 191 }); 192 it('renders a humanized duration for sane durations', () => { 193 expect(formatRelative('2019-11-04T09:15:29.578701-07:00', parseTime('2019-11-04T09:15:35.8701-07:00'))).toEqual( 194 '6.292s' 195 ); 196 expect(formatRelative('2019-11-04T09:15:35.8701-07:00', parseTime('2019-11-04T09:15:29.578701-07:00'))).toEqual( 197 '-6.292s' 198 ); 199 }); 200 }); 201 }); 202 203 describe('URL Params', () => { 204 const stores: any = [ 205 { 206 name: 'thanos_sidecar_one:10901', 207 }, 208 ]; 209 210 const panels: any = [ 211 { 212 key: '0', 213 options: { 214 endTime: 1572046620000, 215 expr: 'rate(node_cpu_seconds_total{mode="system"}[1m])', 216 range: 60 * 60 * 1000, 217 resolution: null, 218 stacked: false, 219 maxSourceResolution: 'raw', 220 useDeduplication: true, 221 usePartialResponse: false, 222 type: PanelType.Graph, 223 storeMatches: [], 224 engine: 'prometheus', 225 explain: false, 226 }, 227 }, 228 { 229 key: '1', 230 options: { 231 endTime: null, 232 expr: 'node_filesystem_avail_bytes', 233 range: 60 * 60 * 1000, 234 resolution: null, 235 stacked: false, 236 maxSourceResolution: 'auto', 237 useDeduplication: false, 238 usePartialResponse: true, 239 type: PanelType.Table, 240 storeMatches: stores, 241 engine: 'prometheus', 242 explain: false, 243 }, 244 }, 245 ]; 246 const query = 247 '?g0.expr=rate(node_cpu_seconds_total%7Bmode%3D%22system%22%7D%5B1m%5D)&g0.tab=0&g0.stacked=0&g0.range_input=1h&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.store_matches=%5B%5D&g0.engine=prometheus&g0.explain=0&g0.end_input=2019-10-25%2023%3A37%3A00&g0.moment_input=2019-10-25%2023%3A37%3A00&g1.expr=node_filesystem_avail_bytes&g1.tab=1&g1.stacked=0&g1.range_input=1h&g1.max_source_resolution=auto&g1.deduplicate=0&g1.partial_response=1&g1.store_matches=%5B%7B%22name%22%3A%22thanos_sidecar_one%3A10901%22%7D%5D&g1.engine=prometheus&g1.explain=0'; 248 249 describe('decodePanelOptionsFromQueryString', () => { 250 it('returns [] when query is empty', () => { 251 expect(decodePanelOptionsFromQueryString('')).toEqual([]); 252 }); 253 it('returns and array of parsed params when query string is non-empty', () => { 254 expect(decodePanelOptionsFromQueryString(query)).toMatchObject(panels); 255 }); 256 }); 257 258 describe('parseOption', () => { 259 it('should return empty object for invalid param', () => { 260 expect(parseOption('invalid_prop=foo')).toEqual({}); 261 }); 262 it('should parse expr param', () => { 263 expect(parseOption('expr=foo')).toEqual({ expr: 'foo' }); 264 }); 265 it('should parse stacked', () => { 266 expect(parseOption('stacked=1')).toEqual({ stacked: true }); 267 }); 268 it('should parse end_input', () => { 269 expect(parseOption('end_input=2019-10-25%2023%3A37')).toEqual({ endTime: moment.utc('2019-10-25 23:37').valueOf() }); 270 }); 271 it('should parse moment_input', () => { 272 expect(parseOption('moment_input=2019-10-25%2023%3A37')).toEqual({ 273 endTime: moment.utc('2019-10-25 23:37').valueOf(), 274 }); 275 }); 276 277 it('should parse max source res', () => { 278 expect(parseOption('max_source_resolution=auto')).toEqual({ maxSourceResolution: 'auto' }); 279 }); 280 it('should parse use deduplicate', () => { 281 expect(parseOption('deduplicate=1')).toEqual({ useDeduplication: true }); 282 }); 283 it('should parse partial_response', () => { 284 expect(parseOption('partial_response=1')).toEqual({ usePartialResponse: true }); 285 }); 286 it('it should parse store_matches', () => { 287 expect(parseOption('store_matches=%5B%7B%22name%22%3A%22thanos_sidecar_one%3A10901%22%7D%5D')).toEqual({ 288 storeMatches: stores, 289 }); 290 }); 291 292 describe('step_input', () => { 293 it('should return step_input parsed if > 0', () => { 294 expect(parseOption('step_input=2')).toEqual({ resolution: 2 }); 295 }); 296 it('should return empty object if step is equal 0', () => { 297 expect(parseOption('step_input=0')).toEqual({}); 298 }); 299 }); 300 301 describe('range_input', () => { 302 it('should return range parsed if its not null', () => { 303 expect(parseOption('range_input=2h')).toEqual({ range: 2 * 60 * 60 * 1000 }); 304 }); 305 it('should return empty object for invalid value', () => { 306 expect(parseOption('range_input=h')).toEqual({}); 307 }); 308 }); 309 310 describe('Parse type param', () => { 311 it('should return panel type "graph" if tab=0', () => { 312 expect(parseOption('tab=0')).toEqual({ type: PanelType.Graph }); 313 }); 314 it('should return panel type "table" if tab=1', () => { 315 expect(parseOption('tab=1')).toEqual({ type: PanelType.Table }); 316 }); 317 }); 318 }); 319 320 describe('toQueryString', () => { 321 it('should generate query string from panel options', () => { 322 expect( 323 toQueryString({ 324 id: 'asdf', 325 key: '0', 326 options: { 327 expr: 'foo', 328 type: PanelType.Graph, 329 stacked: true, 330 range: 0, 331 endTime: null, 332 resolution: 1, 333 maxSourceResolution: 'raw', 334 useDeduplication: true, 335 usePartialResponse: false, 336 storeMatches: [], 337 engine: 'prometheus', 338 explain: false, 339 disableExplainCheckbox: false, 340 }, 341 }) 342 ).toEqual( 343 'g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0s&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.store_matches=%5B%5D&g0.engine=prometheus&g0.explain=0&g0.step_input=1' 344 ); 345 }); 346 }); 347 348 describe('encodePanelOptionsToQueryString', () => { 349 it('returns ? when panels is empty', () => { 350 expect(encodePanelOptionsToQueryString([])).toEqual('?'); 351 }); 352 it('returns an encoded query string otherwise', () => { 353 expect(encodePanelOptionsToQueryString(panels)).toEqual(query); 354 }); 355 }); 356 }); 357 });