Repository: open-mmlab/mmdetection

Issue:
[Bug] metric should be one of \\\'bbox\\\' when using tools/test.py
### Prerequisite

- [X] I have searched [Issues](https://github.com/open-mmlab/mmdetection/issues) and [Discussions](https://github.com/open-mmlab/mmdetection/discussions) but cannot get the expected help.
- [X] I have read the [FAQ documentation](https://mmdetection.readthedocs.io/en/latest/faq.html) but cannot get the expected help.
- [X] The bug has not been fixed in the [latest version (master)](https://github.com/open-mmlab/mmdetection) or [latest version (3.x)](https://github.com/open-mmlab/mmdetection/tree/dev-3.x).

### Task

I have modified the scripts/configs, or I'm working on my own tasks/models/datasets.

### Branch

3.x branch https://github.com/open-mmlab/mmdetection/tree/3.x

### Environment

OrderedDict([('sys.platform', 'linux'),
             ('Python',
              '3.7.12 | packaged by conda-forge | (default, Oct 26 2021, 06:08:21) [GCC 9.4.0]'),
             ('CUDA available', True),
             ('numpy_random_seed', 2147483648),
             ('GPU 0', 'NVIDIA GeForce RTX 3080'),
             ('CUDA_HOME', '/usr/local/cuda-11.5'),
             ('NVCC', 'Cuda compilation tools, release 11.5, V11.5.50'),
             ('GCC', 'gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0'),
             ('PyTorch', '1.12.1+cu113'),
             ('PyTorch compiling details',
              'PyTorch built with:\n  - GCC 9.3\n  - C++ Version: 201402\n  - Intel(R) Math Kernel Library Version 2020.0.0 Product Build 20191122 for Intel(R) 64 architecture applications\n  - Intel(R) MKL-DNN v2.6.0 (Git Hash 52b5f107dd9cf10910aaa19cb47f3abf9b349815)\n  - OpenMP 201511 (a.k.a. OpenMP 4.5)\n  - LAPACK is enabled (usually provided by MKL)\n  - NNPACK is enabled\n  - CPU capability usage: AVX2\n  - CUDA Runtime 11.3\n  - NVCC architecture flags: -gencode;arch=compute_37,code=sm_37;-gencode;arch=compute_50,code=sm_50;-gencode;arch=compute_60,code=sm_60;-gencode;arch=compute_70,code=sm_70;-gencode;arch=compute_75,code=sm_75;-gencode;arch=compute_80,code=sm_80;-gencode;arch=compute_86,code=sm_86\n  - CuDNN 8.3.2  (built against CUDA 11.5)\n  - Magma 2.5.2\n  - Build settings: BLAS_INFO=mkl, BUILD_TYPE=Release, CUDA_VERSION=11.3, CUDNN_VERSION=8.3.2, CXX_COMPILER=/opt/rh/devtoolset-9/root/usr/bin/c++, CXX_FLAGS= -fabi-version=11 -Wno-deprecated -fvisibility-inlines-hidden -DUSE_PTHREADPOOL -fopenmp -DNDEBUG -DUSE_KINETO -DUSE_FBGEMM -DUSE_QNNPACK -DUSE_PYTORCH_QNNPACK -DUSE_XNNPACK -DSYMBOLICATE_MOBILE_DEBUG_HANDLE -DEDGE_PROFILER_USE_KINETO -O2 -fPIC -Wno-narrowing -Wall -Wextra -Werror=return-type -Wno-missing-field-initializers -Wno-type-limits -Wno-array-bounds -Wno-unknown-pragmas -Wno-unused-parameter -Wno-unused-function -Wno-unused-result -Wno-unused-local-typedefs -Wno-strict-overflow -Wno-strict-aliasing -Wno-error=deprecated-declarations -Wno-stringop-overflow -Wno-psabi -Wno-error=pedantic -Wno-error=redundant-decls -Wno-error=old-style-cast -fdiagnostics-color=always -faligned-new -Wno-unused-but-set-variable -Wno-maybe-uninitialized -fno-math-errno -fno-trapping-math -Werror=format -Werror=cast-function-type -Wno-stringop-overflow, LAPACK_INFO=mkl, PERF_WITH_AVX=1, PERF_WITH_AVX2=1, PERF_WITH_AVX512=1, TORCH_VERSION=1.12.1, USE_CUDA=ON, USE_CUDNN=ON, USE_EXCEPTION_PTR=1, USE_GFLAGS=OFF, USE_GLOG=OFF, USE_MKL=ON, USE_MKLDNN=OFF, USE_MPI=OFF, USE_NCCL=ON, USE_NNPACK=ON, USE_OPENMP=ON, USE_ROCM=OFF, \n'),
             ('TorchVision', '0.13.1+cu113'),
             ('OpenCV', '4.6.0'),
             ('MMEngine', '0.2.0'),
             ('MMDetection', '3.0.0rc2+5897b32')])

### Reproduces the problem - code sample

Here is my config:
```python
test_evaluator = dict(
    type='CocoMetric',
    ann_file='./ballondatasets/balloon/val/annotation_coco.json',
    metric=['bbox', 'segm'],
    format_only=False)
```

if i use `python config checkpoint --out result.pkl`, it will be modified to 
```
```python
test_evaluator = dict(
    type='CocoMetric',
    ann_file='./ballondatasets/balloon/val/annotation_coco.json',
    metric=[None, {
        'type': 'DumpResults',
        'out_file_path': 'ceshi.pkl'
    }],
    format_only=False)
```


### Reproduces the problem - error message

```
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 1533, in build_test_loop
    evaluator=self._test_evaluator))
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/registry/registry.py", line 421, in build
    return self.build_func(cfg, *args, **kwargs, registry=self)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/registry/build_functions.py", line 136, in build_from_cfg
    f'class `{obj_cls.__name__}` in '  # type: ignore
KeyError: 'class `TestLoop` in mmengine/runner/loops.py: \'class `CocoMetric` in mmdet/evaluation/metrics/coco_metric.py: "metric should be one of \\\'bbox\\\', \\\'segm\\\', \\\'proposal\\\', \\\'proposal_fast\\\', but got {\\\'type\\\': \\\'DumpResults\\\', \\\'out_file_path\\\': \\\'ceshi.pkl\\\'}."\''

```
### Additional information

I use the tools/test.py to output pkl file but i got the problem,the python file will modify my conifg.

I see the mmdet documentation says that we can use --eval bbox or segm to specify the metric parameter. But the tools/test.py doesn't has the `--eval` option.I don't know if 3.x have implemented the function.


Discussion:
Haven't met your issue. The test_evaluator will be a list, such as:
![image](https://user-images.githubusercontent.com/48282753/199927474-d8cf8adb-28db-45df-bc01-bd2185953cad.png)

Try to update your mmdetection version, add check whether you have changed some codes.

--eval is 2.x args, which is not in 3.x
> Haven't met your issue. The test_evaluator will be a list, such as: ![image](https://user-images.githubusercontent.com/48282753/199927474-d8cf8adb-28db-45df-bc01-bd2185953cad.png)
> 
> Try to update your mmdetection version, add check whether you have changed some codes.
> 
> --eval is 2.x args, which is not in 3.x

My mmdet version is 3.x.

I get it , the cn document is still old api.
https://mmdetection.readthedocs.io/zh_CN/3.x/user_guides/test.html


Here is my config:
``` python
model = dict(
    type='MaskRCNN',
    data_preprocessor=dict(
        type='DetDataPreprocessor',
        mean=[103.53, 116.28, 123.675],
        std=[1.0, 1.0, 1.0],
        bgr_to_rgb=False,
        pad_mask=True,
        pad_size_divisor=32),
    backbone=dict(
        type='ResNet',
        depth=50,
        num_stages=4,
        out_indices=(0, 1, 2, 3),
        frozen_stages=1,
        norm_cfg=dict(type='BN', requires_grad=False),
        norm_eval=True,
        style='caffe',
        init_cfg=dict(
            type='Pretrained',
            checkpoint='open-mmlab://detectron2/resnet50_caffe')),
    neck=dict(
        type='FPN',
        in_channels=[256, 512, 1024, 2048],
        out_channels=256,
        num_outs=5),
    rpn_head=dict(
        type='RPNHead',
        in_channels=256,
        feat_channels=256,
        anchor_generator=dict(
            type='AnchorGenerator',
            scales=[8],
            ratios=[0.5, 1.0, 2.0],
            strides=[4, 8, 16, 32, 64]),
        bbox_coder=dict(
            type='DeltaXYWHBBoxCoder',
            target_means=[0.0, 0.0, 0.0, 0.0],
            target_stds=[1.0, 1.0, 1.0, 1.0]),
        loss_cls=dict(
            type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0),
        loss_bbox=dict(type='L1Loss', loss_weight=1.0)),
    roi_head=dict(
        type='StandardRoIHead',
        bbox_roi_extractor=dict(
            type='SingleRoIExtractor',
            roi_layer=dict(type='RoIAlign', output_size=7, sampling_ratio=0),
            out_channels=256,
            featmap_strides=[4, 8, 16, 32]),
        bbox_head=dict(
            type='Shared2FCBBoxHead',
            in_channels=256,
            fc_out_channels=1024,
            roi_feat_size=7,
            num_classes=1,
            bbox_coder=dict(
                type='DeltaXYWHBBoxCoder',
                target_means=[0.0, 0.0, 0.0, 0.0],
                target_stds=[0.1, 0.1, 0.2, 0.2]),
            reg_class_agnostic=False,
            loss_cls=dict(
                type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0),
            loss_bbox=dict(type='L1Loss', loss_weight=1.0)),
        mask_roi_extractor=dict(
            type='SingleRoIExtractor',
            roi_layer=dict(type='RoIAlign', output_size=14, sampling_ratio=0),
            out_channels=256,
            featmap_strides=[4, 8, 16, 32]),
        mask_head=dict(
            type='FCNMaskHead',
            num_convs=4,
            in_channels=256,
            conv_out_channels=256,
            num_classes=1,
            loss_mask=dict(
                type='CrossEntropyLoss', use_mask=True, loss_weight=1.0))),
    train_cfg=dict(
        rpn=dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.7,
                neg_iou_thr=0.3,
                min_pos_iou=0.3,
                match_low_quality=True,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=256,
                pos_fraction=0.5,
                neg_pos_ub=-1,
                add_gt_as_proposals=False),
            allowed_border=-1,
            pos_weight=-1,
            debug=False),
        rpn_proposal=dict(
            nms_pre=2000,
            max_per_img=1000,
            nms=dict(type='nms', iou_threshold=0.7),
            min_bbox_size=0),
        rcnn=dict(
            assigner=dict(
                type='MaxIoUAssigner',
                pos_iou_thr=0.5,
                neg_iou_thr=0.5,
                min_pos_iou=0.5,
                match_low_quality=True,
                ignore_iof_thr=-1),
            sampler=dict(
                type='RandomSampler',
                num=512,
                pos_fraction=0.25,
                neg_pos_ub=-1,
                add_gt_as_proposals=True),
            mask_size=28,
            pos_weight=-1,
            debug=False)),
    test_cfg=dict(
        rpn=dict(
            nms_pre=1000,
            max_per_img=1000,
            nms=dict(type='nms', iou_threshold=0.7),
            min_bbox_size=0),
        rcnn=dict(
            score_thr=0.05,
            nms=dict(type='nms', iou_threshold=0.5),
            max_per_img=100,
            mask_thr_binary=0.5)))
dataset_type = 'CocoDataset'
data_root = './ballondatasets/balloon'
file_client_args = dict(backend='disk')
train_pipeline = [
    dict(type='LoadImageFromFile', file_client_args=dict(backend='disk')),
    dict(
        type='LoadAnnotations',
        with_bbox=True,
        with_mask=True,
        poly2mask=False),
    dict(
        type='RandomChoiceResize',
        scales=[(1333, 640), (1333, 672), (1333, 704), (1333, 736),
                (1333, 768), (1333, 800)],
        keep_ratio=True),
    dict(type='RandomFlip', prob=0.5),
    dict(type='PackDetInputs')
]
test_pipeline = [
    dict(type='LoadImageFromFile', file_client_args=dict(backend='disk')),
    dict(type='Resize', scale=(1333, 800), keep_ratio=True),
    dict(type='LoadAnnotations', with_bbox=True, with_mask=True),
    dict(
        type='PackDetInputs',
        meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape',
                   'scale_factor'))
]
train_dataloader = dict(
    batch_size=2,
    num_workers=2,
    persistent_workers=True,
    sampler=dict(type='DefaultSampler', shuffle=True),
    batch_sampler=dict(type='AspectRatioBatchSampler'),
    dataset=dict(
        type='CocoDataset',
        data_root='./ballondatasets/balloon',
        ann_file='train/annotation_coco.json',
        data_prefix=dict(img='train/'),
        filter_cfg=dict(filter_empty_gt=True, min_size=32),
        pipeline=[
            dict(
                type='LoadImageFromFile',
                file_client_args=dict(backend='disk')),
            dict(
                type='LoadAnnotations',
                with_bbox=True,
                with_mask=True,
                poly2mask=False),
            dict(
                type='RandomChoiceResize',
                scales=[(1333, 640), (1333, 672), (1333, 704), (1333, 736),
                        (1333, 768), (1333, 800)],
                keep_ratio=True),
            dict(type='RandomFlip', prob=0.5),
            dict(type='PackDetInputs')
        ],
        metainfo=dict(CLASSES=('balloon', ), PALETTE=[(220, 20, 60)])))
val_dataloader = dict(
    batch_size=1,
    num_workers=2,
    persistent_workers=True,
    drop_last=False,
    sampler=dict(type='DefaultSampler', shuffle=False),
    dataset=dict(
        type='CocoDataset',
        data_root='./ballondatasets/balloon',
        ann_file='val/annotation_coco.json',
        data_prefix=dict(img='val/'),
        test_mode=True,
        pipeline=[
            dict(
                type='LoadImageFromFile',
                file_client_args=dict(backend='disk')),
            dict(type='Resize', scale=(1333, 800), keep_ratio=True),
            dict(type='LoadAnnotations', with_bbox=True, with_mask=True),
            dict(
                type='PackDetInputs',
                meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape',
                           'scale_factor'))
        ],
        metainfo=dict(CLASSES=('balloon', ), PALETTE=[(220, 20, 60)])))
test_dataloader = dict(
    batch_size=1,
    num_workers=2,
    persistent_workers=True,
    drop_last=False,
    sampler=dict(type='DefaultSampler', shuffle=False),
    dataset=dict(
        type='CocoDataset',
        data_root='./ballondatasets/balloon',
        ann_file='val/annotation_coco.json',
        data_prefix=dict(img='val/'),
        test_mode=True,
        pipeline=[
            dict(
                type='LoadImageFromFile',
                file_client_args=dict(backend='disk')),
            dict(type='Resize', scale=(1333, 800), keep_ratio=True),
            dict(type='LoadAnnotations', with_bbox=True, with_mask=True),
            dict(
                type='PackDetInputs',
                meta_keys=('img_id', 'img_path', 'ori_shape', 'img_shape',
                           'scale_factor'))
        ],
        metainfo=dict(CLASSES=('balloon', ), PALETTE=[(220, 20, 60)])))
val_evaluator = dict(
    type='CocoMetric',
    ann_file='./ballondatasets/balloon/val/annotation_coco.json',
    metric=['bbox', 'segm'],
    format_only=False)
test_evaluator = dict(
    type='CocoMetric',
    ann_file='./ballondatasets/balloon/val/annotation_coco.json',
    metric=['bbox', 'segm'],
    format_only=False)
train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=12, val_interval=3)
val_cfg = dict(type='ValLoop')
test_cfg = dict(type='TestLoop')
param_scheduler = [
    dict(
        type='LinearLR', start_factor=0.001, by_epoch=False, begin=0, end=500),
    dict(
        type='MultiStepLR',
        begin=0,
        end=12,
        by_epoch=True,
        milestones=[8, 11],
        gamma=0.1)
]
optim_wrapper = dict(
    type='OptimWrapper',
    optimizer=dict(type='SGD', lr=0.0025, momentum=0.9, weight_decay=0.0001))
auto_scale_lr = dict(enable=False, base_batch_size=16)
default_scope = 'mmdet'
default_hooks = dict(
    timer=dict(type='IterTimerHook'),
    logger=dict(type='LoggerHook', interval=10),
    param_scheduler=dict(type='ParamSchedulerHook'),
    checkpoint=dict(type='CheckpointHook', interval=3),
    sampler_seed=dict(type='DistSamplerSeedHook'),
    visualization=dict(type='DetVisualizationHook'))
env_cfg = dict(
    cudnn_benchmark=False,
    mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0),
    dist_cfg=dict(backend='nccl'))
vis_backends = [dict(type='LocalVisBackend')]
visualizer = dict(
    type='DetLocalVisualizer',
    vis_backends=[
        dict(type='LocalVisBackend'),
        dict(type='TensorboardVisBackend')
    ],
    name='visualizer')
log_processor = dict(type='LogProcessor', window_size=50, by_epoch=True)
log_level = 'DEBUG'
load_from = 'checkpoints/mask_rcnn_r50_caffe_fpn_mstrain-poly_3x_coco_bbox_mAP-0.408__segm_mAP-0.37_20200504_163245-42aa3d00.pth'
resume = False
metainfo = dict(CLASSES=('balloon', ), PALETTE=[(220, 20, 60)])
work_dir = './tutorial_exps'

```
after i run the test, the `test_evaluator` will be modified to:
```  python
test_evaluator = dict(
    type='CocoMetric',
    ann_file='./ballondatasets/balloon/val/annotation_coco.json',
    metric=[None, {
        'type': 'DumpResults',
        'out_file_path': 'ceshi.pkl'
    }],
    format_only=False)
```

Thank you for your debug info, I will try to fix it.
Oh, I got your error, this seems a bug here. We will fix ASAP
![image](https://user-images.githubusercontent.com/48282753/199931398-a8cb9f44-1492-4b0d-8269-580c6e0f9aab.png)
You can try to change:

```
 elif isinstance(cfg.test_evaluator.metric, (list, tuple)):
            cfg.test_evaluator.metric = list(
                cfg.test_evaluator.metric).append(dump_metric)
```
to 
```
elif isinstance(cfg.test_evaluator.metric, (list, tuple)):
    cfg.test_evaluator = [cfg.test_evaluator, dump_metric]
```
> ![image](https://user-images.githubusercontent.com/48282753/199931398-a8cb9f44-1492-4b0d-8269-580c6e0f9aab.png) You can try to change:
> 
> ```
>  elif isinstance(cfg.test_evaluator.metric, (list, tuple)):
>             cfg.test_evaluator.metric = list(
>                 cfg.test_evaluator.metric).append(dump_metric)
> ```
> 
> to
> 
> ```
> elif isinstance(cfg.test_evaluator.metric, (list, tuple)):
>     cfg.test_evaluator = [cfg.test_evaluator, dump_metric]
> ```

Thank you so much, now it looks good
``` python 
test_evaluator = [
    dict(
        type='CocoMetric',
        ann_file='./ballondatasets/balloon/val/annotation_coco.json',
        metric=['bbox', 'segm'],
        format_only=False),
    dict(type='DumpResults', out_file_path='mytest.pkl')
]
```
but i meet the different problem:
```
Traceback (most recent call last):
  File "tools/test.py", line 105, in <module>
    main()
  File "tools/test.py", line 101, in main
    runner.test()
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 1703, in test
    self.load_or_resume()
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 1600, in load_or_resume
    self.load_checkpoint(self._load_from)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 2006, in load_checkpoint
    checkpoint = _load_checkpoint(filename, map_location=map_location)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/checkpoint.py", line 463, in _load_checkpoint
    return CheckpointLoader.load_checkpoint(filename, map_location, logger)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/checkpoint.py", line 257, in load_checkpoint
    return checkpoint_loader(filename, map_location)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/checkpoint.py", line 274, in load_from_local
    checkpoint = torch.load(filename, map_location=map_location)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/torch/serialization.py", line 713, in load
    return _legacy_load(opened_file, map_location, pickle_module, **pickle_load_args)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/torch/serialization.py", line 920, in _legacy_load
    magic_number = pickle_module.load(f, **pickle_load_args)
_pickle.UnpicklingError: invalid load key, '/'.
```

I run it again `python tools/test.py tutorial_exps/mask-rcnn_r50-caffe_fpn_ms-poly-coco2.py tutorial_exps/epoch_12.pth --out ./mytestpkl.pkl`
 then i get:

```
Traceback (most recent call last):
  File "tools/test.py", line 105, in <module>
    main()
  File "tools/test.py", line 94, in main
    runner = Runner.from_cfg(cfg)
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 459, in from_cfg
    cfg=cfg,
  File "/home/sanbu/anaconda3/envs/mmlab2/lib/python3.7/site-packages/mmengine/runner/runner.py", line 329, in __init__
    'test_dataloader, test_cfg, and test_evaluator should be '
ValueError: test_dataloader, test_cfg, and test_evaluator should be either all None or not None, but got test_dataloader={'batch_size': 1, 'num_workers': 2, 'persistent_workers': True, 'drop_last': False, 'sampler': {'type': 'DefaultSampler', 'shuffle': False}, 'dataset': {'type': 'CocoDataset', 'data_root': './ballondatasets/balloon', 'ann_file': 'val/annotation_coco.json', 'data_prefix': {'img': 'val/'}, 'test_mode': True, 'pipeline': [{'type': 'LoadImageFromFile', 'file_client_args': {'backend': 'disk'}}, {'type': 'Resize', 'scale': (1333, 800), 'keep_ratio': True}, {'type': 'LoadAnnotations', 'with_bbox': True, 'with_mask': True}, {'type': 'PackDetInputs', 'meta_keys': ('img_id', 'img_path', 'ori_shape', 'img_shape', 'scale_factor')}], 'metainfo': {'CLASSES': ('balloon',), 'PALETTE': [(220, 20, 60)]}}}, test_cfg={'type': 'TestLoop'}, test_evaluator=None

```

Resolving patch:
diff --git a/mmdet/utils/__init__.py b/mmdet/utils/__init__.py
--- a/mmdet/utils/__init__.py
+++ b/mmdet/utils/__init__.py
@@ -5,7 +5,7 @@
                          sync_random_seed)
 from .logger import get_caller_name, log_img_scale
 from .memory import AvoidCUDAOOM, AvoidOOM
-from .misc import add_dump_metric, find_latest_checkpoint, update_data_root
+from .misc import find_latest_checkpoint, update_data_root
 from .replace_cfg_vals import replace_cfg_vals
 from .setup_env import register_all_modules, setup_multi_processes
 from .split_batch import split_batch
@@ -20,5 +20,5 @@
     'AvoidCUDAOOM', 'all_reduce_dict', 'allreduce_grads', 'reduce_mean',
     'sync_random_seed', 'ConfigType', 'InstanceList', 'MultiConfig',
     'OptConfigType', 'OptInstanceList', 'OptMultiConfig', 'OptPixelList',
-    'PixelList', 'RangeType', 'add_dump_metric'
+    'PixelList', 'RangeType'
 ]
diff --git a/mmdet/utils/misc.py b/mmdet/utils/misc.py
--- a/mmdet/utils/misc.py
+++ b/mmdet/utils/misc.py
@@ -74,19 +74,3 @@ def update(cfg, src_str, dst_str):
 
     update(cfg.data, cfg.data_root, dst_root)
     cfg.data_root = dst_root
-
-
-def add_dump_metric(args, cfg):
-    dump_metric = dict(type='DumpResults', out_file_path=args.out)
-    if isinstance(cfg.test_evaluator, (list, tuple)):
-        cfg.test_evaluator = list(cfg.test_evaluator).append(dump_metric)
-    elif isinstance(cfg.test_evaluator, dict):
-        if isinstance(cfg.test_evaluator.metric, str):
-            cfg.test_evaluator = [cfg.test_evaluator, dump_metric]
-        elif isinstance(cfg.test_evaluator.metric, (list, tuple)):
-            cfg.test_evaluator.metric = list(
-                cfg.test_evaluator.metric).append(dump_metric)
-        else:
-            cfg.test_evaluator.metric = [
-                cfg.test_evaluator.metric, dump_metric
-            ]


Test patch:
diff --git a/tools/analysis_tools/test_robustness.py b/tools/analysis_tools/test_robustness.py
--- a/tools/analysis_tools/test_robustness.py
+++ b/tools/analysis_tools/test_robustness.py
@@ -6,12 +6,13 @@
 
 from mmengine.config import Config, DictAction
 from mmengine.dist import get_dist_info
+from mmengine.evaluator import DumpResults
 from mmengine.fileio import dump
 from mmengine.runner import Runner
 
 from mmdet.engine.hooks.utils import trigger_visualization_hook
 from mmdet.registry import RUNNERS
-from mmdet.utils import add_dump_metric, register_all_modules
+from mmdet.utils import register_all_modules
 from tools.analysis_tools.robustness_eval import get_results
 
 
@@ -118,12 +119,6 @@ def main():
         cfg.work_dir = osp.join('./work_dirs',
                                 osp.splitext(osp.basename(args.config))[0])
 
-    # Dump predictions
-    if args.out is not None:
-        assert args.out.endswith(('.pkl', '.pickle')), \
-            'The dump file must be a pkl file.'
-        add_dump_metric(args, cfg)
-
     cfg.model.backbone.init_cfg.type = None
     cfg.test_dataloader.dataset.test_mode = True
 
@@ -140,6 +135,13 @@ def main():
         # if 'runner_type' is set in the cfg
         runner = RUNNERS.build(cfg)
 
+    # add `DumpResults` dummy metric
+    if args.out is not None:
+        assert args.out.endswith(('.pkl', '.pickle')), \
+            'The dump file must be a pkl file.'
+        runner.test_evaluator.metrics.append(
+            DumpResults(out_file_path=args.out))
+
     if 'all' in args.corruptions:
         corruptions = [
             'gaussian_noise', 'shot_noise', 'impulse_noise', 'defocus_blur',
diff --git a/tools/test.py b/tools/test.py
--- a/tools/test.py
+++ b/tools/test.py
@@ -4,11 +4,12 @@
 import os.path as osp
 
 from mmengine.config import Config, DictAction
+from mmengine.evaluator import DumpResults
 from mmengine.runner import Runner
 
 from mmdet.engine.hooks.utils import trigger_visualization_hook
 from mmdet.registry import RUNNERS
-from mmdet.utils import add_dump_metric, register_all_modules
+from mmdet.utils import register_all_modules
 
 
 # TODO: support fuse_conv_bn and format_only
@@ -82,12 +83,6 @@ def main():
     if args.show or args.show_dir:
         cfg = trigger_visualization_hook(cfg, args)
 
-    # Dump predictions
-    if args.out is not None:
-        assert args.out.endswith(('.pkl', '.pickle')), \
-            'The dump file must be a pkl file.'
-        add_dump_metric(args, cfg)
-
     # build the runner from config
     if 'runner_type' not in cfg:
         # build the default runner
@@ -97,6 +92,13 @@ def main():
         # if 'runner_type' is set in the cfg
         runner = RUNNERS.build(cfg)
 
+    # add `DumpResults` dummy metric
+    if args.out is not None:
+        assert args.out.endswith(('.pkl', '.pickle')), \
+            'The dump file must be a pkl file.'
+        runner.test_evaluator.metrics.append(
+            DumpResults(out_file_path=args.out))
+
     # start testing
     runner.test()