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          '&lt;strong&gt;&#39;example&#39;&amp;&quot;another&#x2F;example&quot;&lt;&#x2F;strong&gt;'
    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  });