Exemplo n.º 1
0
def _artifactor_skip_providers(request, providers, skip_msg):
    skip_data = {
        'type': 'provider',
        'reason': ', '.join(p.key for p in providers),
    }
    fire_art_test_hook(request.node, 'skip_test', skip_data=skip_data)
    pytest.skip(skip_msg)
Exemplo n.º 2
0
    def add_log(self, log_name):
        """ Adds a log file to the merkyl process.

        This function adds a log file path to the merkyl process on the
        appliance. This is relevant only for the duration of the test. At
        the end of the test, the file is removed from the merkyl tracker.

        Note that this is a blocking call, ie, we ensure that the file
        is being logged by merkyl, before we continue. This is important
        and prevents the file_add operation being queued and processes
        which generate log information activating before the log is being
        monitored. This is achieved using the grab_result switch, but
        in fact, nothing will be received.

        It is worth noting that the file path must be "discoverable" by merkyl.
        This may mean editing the allowed_files prior to deploying merkyl.

        Args:
            log_name: Full path to the log file wishing to be monitored.

        """
        fire_art_test_hook(self.node,
                           'add_log_merkyl',
                           ip=self.ip,
                           filename=log_name,
                           grab_result=True)
Exemplo n.º 3
0
def _artifactor_skip_providers(request, providers, skip_msg):
    skip_data = {
        'type': 'provider',
        'reason': ', '.join(p.key for p in providers),
    }
    fire_art_test_hook(request.node, 'skip_test', skip_data=skip_data)
    pytest.skip(skip_msg)
Exemplo n.º 4
0
 def _take_screenshot(name):
     logger.info("Taking a screenshot named {}".format(name))
     ss, ss_error = take_browser_screenshot()
     g_id = fauxfactory.gen_alpha(length=6)
     if ss:
         fire_art_test_hook(item,
                            'filedump',
                            description="Screenshot {}".format(name),
                            file_type="screenshot",
                            mode="wb",
                            contents_base64=True,
                            contents=ss,
                            display_glyph="camera",
                            group_id="fix-screenshot-{}".format(g_id),
                            slaveid=store.slaveid)
     if ss_error:
         fire_art_test_hook(item,
                            'filedump',
                            description="Screenshot error {}".format(name),
                            mode="w",
                            contents_base64=False,
                            contents=ss_error,
                            display_type="danger",
                            group_id="fix-screenshot-{}".format(g_id),
                            slaveid=store.slaveid)
Exemplo n.º 5
0
def resolve_blockers(item, blockers):
    if not isinstance(blockers, (list, tuple, set)):
        raise ValueError("Type of the 'blockers' parameter must be one of: list, tuple, set")

    # Prepare the global env for the kwarg insertion
    appliance = find_appliance(item)
    global_env = dict(
        appliance_version=appliance.version,
        appliance_downstream=appliance.is_downstream,
        item=item,
        blockers=blockers,
    )
    # We will now extend the env with fixtures, so they can be used in the guard functions
    # We will however add only those that are not in the global_env otherwise we could overwrite
    # our own stuff.
    params = extract_fixtures_values(item)
    for funcarg, value in params.items():
        if funcarg not in global_env:
            global_env[funcarg] = value

    # Check blockers
    use_blockers = []
    # Bugzilla shortcut
    blockers = map(lambda b: "BZ#{}".format(b) if isinstance(b, int) else b, blockers)
    for blocker in map(Blocker.parse, blockers):
        if blocker.blocks:
            use_blockers.append(blocker)
    # Unblocking
    discard_blockers = set([])
    for blocker in use_blockers:
        unblock_func = kwargify(blocker.kwargs.get("unblock"))
        local_env = {"blocker": blocker}
        local_env.update(global_env)
        if unblock_func(**local_env):
            discard_blockers.add(blocker)
    for blocker in discard_blockers:
        use_blockers.remove(blocker)
    # We now have those that block testing, so we have to skip
    # Let's go in the order that they were added
    # Custom actions first
    for blocker in use_blockers:
        if "custom_action" in blocker.kwargs:
            action = kwargify(blocker.kwargs["custom_action"])
            local_env = {"blocker": blocker}
            local_env.update(global_env)
            action(**local_env)
    # And then skip
    if use_blockers:
        bugs = [bug.bug_id for bug in use_blockers if hasattr(bug, "bug_id")]
        skip_data = {'type': 'blocker', 'reason': bugs}
        fire_art_test_hook(item, 'skip_test', skip_data=skip_data)
        pytest.skip("Skipping due to these blockers:\n{}".format(
            "\n".join(
                "- {}".format(str(blocker))
                for blocker
                in use_blockers
            )
        ))
Exemplo n.º 6
0
def resolve_blockers(item, blockers):
    if not isinstance(blockers, (list, tuple, set)):
        raise ValueError("Type of the 'blockers' parameter must be one of: list, tuple, set")

    # Prepare the global env for the kwarg insertion
    appliance = find_appliance(item)
    global_env = dict(
        appliance_version=appliance.version,
        appliance_downstream=appliance.is_downstream,
        item=item,
        blockers=blockers,
    )
    # We will now extend the env with fixtures, so they can be used in the guard functions
    # We will however add only those that are not in the global_env otherwise we could overwrite
    # our own stuff.
    params = extract_fixtures_values(item)
    for funcarg, value in params.items():
        if funcarg not in global_env:
            global_env[funcarg] = value

    # Check blockers
    use_blockers = []
    # Bugzilla shortcut
    blockers = map(lambda b: "BZ#{}".format(b) if isinstance(b, int) else b, blockers)
    for blocker in map(Blocker.parse, blockers):
        if blocker.blocks:
            use_blockers.append(blocker)
    # Unblocking
    discard_blockers = set([])
    for blocker in use_blockers:
        unblock_func = kwargify(blocker.kwargs.get("unblock"))
        local_env = {"blocker": blocker}
        local_env.update(global_env)
        if unblock_func(**local_env):
            discard_blockers.add(blocker)
    for blocker in discard_blockers:
        use_blockers.remove(blocker)
    # We now have those that block testing, so we have to skip
    # Let's go in the order that they were added
    # Custom actions first
    for blocker in use_blockers:
        if "custom_action" in blocker.kwargs:
            action = kwargify(blocker.kwargs["custom_action"])
            local_env = {"blocker": blocker}
            local_env.update(global_env)
            action(**local_env)
    # And then skip
    if use_blockers:
        bugs = [bug.bug_id for bug in use_blockers if hasattr(bug, "bug_id")]
        skip_data = {'type': 'blocker', 'reason': bugs}
        fire_art_test_hook(item, 'skip_test', skip_data=skip_data)
        pytest.skip("Skipping due to these blockers:\n{}".format(
            "\n".join(
                "- {}".format(str(blocker))
                for blocker
                in use_blockers
            )
        ))
Exemplo n.º 7
0
    def reset_log(self, log_name):
        """ Resets log

        This function clears content of a log file that merkyl is tailing.
        Note that file stays open and merkyl keeps tailing it.

        Args:
            log_name: Full path to the log file that you want to reset
        """
        fire_art_test_hook(
            self.node, 'reset_log_merkyl', ip=self.ip,
            filename=log_name, grab_result=True)
Exemplo n.º 8
0
def save_screenshot(node, ss, sse):
    if ss:
        fire_art_test_hook(
            node, 'filedump',
            description="RBAC Screenshot", file_type="rbac_screenshot", mode="wb",
            contents_base64=True, contents=ss, display_glyph="camera", group_id="RBAC",
            slaveid=store.slaveid)
    if sse:
        fire_art_test_hook(
            node, 'filedump',
            description="RBAC Screenshot error", file_type="rbac_screenshot_error", mode="w",
            contents_base64=False, contents=sse, display_type="danger", group_id="RBAC",
            slaveid=store.slaveid)
Exemplo n.º 9
0
def save_traceback_file(node, contents):
    """A convenience function for artifactor file sending

    This function simply takes the nodes id and the contents of the file and processes
    them and sends them to artifactor

    Args:
        node: A pytest node
        contents: The contents of the traceback file
    """
    fire_art_test_hook(
        node, 'filedump',
        description="RBAC Traceback",
        contents=contents, file_type="rbac", group_id="RBAC", slaveid=store.slaveid)
Exemplo n.º 10
0
 def _take_screenshot(name):
     logger.info("Taking a screenshot named {}".format(name))
     ss, ss_error = take_browser_screenshot()
     g_id = fauxfactory.gen_alpha(length=6)
     if ss:
         fire_art_test_hook(
             item, 'filedump',
             description="Screenshot {}".format(name), file_type="screenshot", mode="wb",
             contents_base64=True, contents=ss, display_glyph="camera",
             group_id="fix-screenshot-{}".format(g_id), slaveid=store.slaveid)
     if ss_error:
         fire_art_test_hook(
             item, 'filedump',
             description="Screenshot error {}".format(name), mode="w", contents_base64=False,
             contents=ss_error, display_type="danger",
             group_id="fix-screenshot-{}".format(g_id), slaveid=store.slaveid)
Exemplo n.º 11
0
def save_traceback_file(node, contents):
    """A convenience function for artifactor file sending

    This function simply takes the nodes id and the contents of the file and processes
    them and sends them to artifactor

    Args:
        node: A pytest node
        contents: The contents of the traceback file
    """
    fire_art_test_hook(node,
                       'filedump',
                       description="RBAC Traceback",
                       contents=contents,
                       file_type="rbac",
                       group_id="RBAC",
                       slaveid=store.slaveid)
Exemplo n.º 12
0
def handle_assert_artifacts(request, fail_message=None):
    appliance = find_appliance(request)
    if isinstance(appliance, DummyAppliance):
        return
    if not fail_message:
        short_tb = '{}'.format(sys.exc_info()[1])
        short_tb = base64_from_text(short_tb)
        var_tb = traceback.format_tb(sys.exc_info()[2])
        full_tb = "".join(var_tb)
        full_tb = base64_from_text(full_tb)

    else:
        short_tb = full_tb = base64_from_text(fail_message)

    try:
        ss = cfme.utils.browser.browser().get_screenshot_as_base64()
        ss_error = None
    except Exception as b_ex:
        ss = None
        if str(b_ex):
            ss_error = '{}: {}'.format(type(b_ex).__name__, str(b_ex))
        else:
            ss_error = type(b_ex).__name__
    if ss_error:
        ss_error = base64_from_text(ss_error)

    # A simple id to match the artifacts together
    sa_id = "softassert-{}".format(fauxfactory.gen_alpha(length=3).upper())
    from cfme.fixtures.pytest_store import store
    node = request.node

    fire_art_test_hook(
        node, 'filedump',
        description="Soft Assert Traceback", contents=full_tb,
        file_type="soft_traceback", display_type="danger", display_glyph="align-justify",
        contents_base64=True, group_id=sa_id, slaveid=store.slaveid)
    fire_art_test_hook(
        node, 'filedump',
        description="Soft Assert Short Traceback", contents=short_tb,
        file_type="soft_short_tb", display_type="danger", display_glyph="align-justify",
        contents_base64=True, group_id=sa_id, slaveid=store.slaveid)
    if ss is not None:
        fire_art_test_hook(
            node, 'filedump',
            description="Soft Assert Exception screenshot",
            file_type="screenshot", mode="wb", contents_base64=True, contents=ss,
            display_glyph="camera", group_id=sa_id, slaveid=store.slaveid)
    if ss_error is not None:
        fire_art_test_hook(
            node, 'filedump',
            description="Soft Assert Screenshot error", mode="w",
            contents_base64=True, contents=ss_error, display_type="danger", group_id=sa_id,
            slaveid=store.slaveid)
Exemplo n.º 13
0
def handle_assert_artifacts(request, fail_message=None):
    appliance = find_appliance(request)
    if isinstance(appliance, DummyAppliance):
        return
    if not fail_message:
        short_tb = '{}'.format(sys.exc_info()[1])
        short_tb = short_tb.encode('base64')
        var_tb = traceback.format_tb(sys.exc_info()[2])
        full_tb = "".join(var_tb)
        full_tb = full_tb.encode('base64')

    else:
        short_tb = full_tb = fail_message.encode('base64')

    try:
        ss = cfme.utils.browser.browser().get_screenshot_as_base64()
        ss_error = None
    except Exception as b_ex:
        ss = None
        if str(b_ex):
            ss_error = '{}: {}'.format(type(b_ex).__name__, str(b_ex))
        else:
            ss_error = type(b_ex).__name__
    if ss_error:
        ss_error = ss_error.encode('base64')

    # A simple id to match the artifacts together
    sa_id = "softassert-{}".format(fauxfactory.gen_alpha(length=3).upper())
    from cfme.fixtures.pytest_store import store
    node = request.node

    fire_art_test_hook(
        node, 'filedump',
        description="Soft Assert Traceback", contents=full_tb,
        file_type="soft_traceback", display_type="danger", display_glyph="align-justify",
        contents_base64=True, group_id=sa_id, slaveid=store.slaveid)
    fire_art_test_hook(
        node, 'filedump',
        description="Soft Assert Short Traceback", contents=short_tb,
        file_type="soft_short_tb", display_type="danger", display_glyph="align-justify",
        contents_base64=True, group_id=sa_id, slaveid=store.slaveid)
    if ss is not None:
        fire_art_test_hook(
            node, 'filedump',
            description="Soft Assert Exception screenshot",
            file_type="screenshot", mode="wb", contents_base64=True, contents=ss,
            display_glyph="camera", group_id=sa_id, slaveid=store.slaveid)
    if ss_error is not None:
        fire_art_test_hook(
            node, 'filedump',
            description="Soft Assert Screenshot error", mode="w",
            contents_base64=True, contents=ss_error, display_type="danger", group_id=sa_id,
            slaveid=store.slaveid)
Exemplo n.º 14
0
def pytest_runtest_teardown(item, nextitem):
    qa_string = "Unknown,None"
    if hasattr(item, "_metadata") and item._metadata.get('owner') is not None:
        # The owner is specified in metadata
        qa_string = "{},from metadata"
    else:
        try:
            qa_arr = []
            results = dig_code(item)
            for idx in range(min(2, len(results))):
                qa_arr.append("{},{:.2f}%\n".format(results[idx][0], results[idx][1]))
            if qa_arr:
                qa_string = "".join(qa_arr)
        except:
            pass
    fire_art_test_hook(
        item,
        'filedump', description="QA Contact",
        contents=str(qa_string), file_type="qa_contact", group_id="qa-contact",
        slaveid=store.slaveid)
Exemplo n.º 15
0
def pytest_runtest_call(item):
    try:
        yield
    finally:
        if "smtp_test" not in (item.funcargs or []):
            return

        try:
            fire_art_test_hook(
                item,
                "filedump",
                description="received e-mails",
                contents=item.funcargs["smtp_test"].get_html_report(),
                file_type="html",
                display_glyph="align-justify",
                group_id="misc-artifacts",
            )
        except Exception as e:
            logger.exception(e)
            logger.error("Something happened to the SMTP collector.")
Exemplo n.º 16
0
def pytest_runtest_call(item):
    try:
        yield
    finally:
        if "smtp_test" not in (item.funcargs or []):
            return

        try:
            fire_art_test_hook(
                item,
                "filedump",
                description="received e-mails",
                contents=item.funcargs["smtp_test"].get_html_report(),
                file_type="html",
                display_glyph="align-justify",
                group_id="misc-artifacts",
            )
        except Exception as e:
            logger.exception(e)
            logger.error("Something happened to the SMTP collector.")
Exemplo n.º 17
0
def pytest_runtest_teardown(item, nextitem):
    qa_string = "Unknown,None"
    if hasattr(item, "_metadata") and item._metadata.get('owner') is not None:
        # The owner is specified in metadata
        qa_string = "{},from metadata"
    else:
        try:
            qa_arr = []
            results = dig_code(item)
            for idx in range(min(2, len(results))):
                qa_arr.append("{},{:.2f}%\n".format(results[idx][0], results[idx][1]))
            if qa_arr:
                qa_string = "".join(qa_arr)
        except:
            pass
    fire_art_test_hook(
        item,
        'filedump', description="QA Contact",
        contents=str(qa_string), file_type="qa_contact", group_id="qa-contact",
        slaveid=store.slaveid)
Exemplo n.º 18
0
    def get_log(self, log_name):
        """ A simple getter for log files.

        Returns the cached content of a particular log

        Args:
            log_name: Full path to the log file wishing to be received.
        """
        res = fire_art_test_hook(
            self.node, 'get_log_merkyl', ip=self.ip,
            filename=log_name, grab_result=True)
        return res['merkyl_content']
Exemplo n.º 19
0
def save_screenshot(node, ss, sse):
    if ss:
        fire_art_test_hook(node,
                           'filedump',
                           description="RBAC Screenshot",
                           file_type="rbac_screenshot",
                           mode="wb",
                           contents_base64=True,
                           contents=ss,
                           display_glyph="camera",
                           group_id="RBAC",
                           slaveid=store.slaveid)
    if sse:
        fire_art_test_hook(node,
                           'filedump',
                           description="RBAC Screenshot error",
                           file_type="rbac_screenshot_error",
                           mode="w",
                           contents_base64=False,
                           contents=sse,
                           display_type="danger",
                           group_id="RBAC",
                           slaveid=store.slaveid)
Exemplo n.º 20
0
    def add_log(self, log_name):
        """ Adds a log file to the merkyl process.

        This function adds a log file path to the merkyl process on the
        appliance. This is relevant only for the duration of the test. At
        the end of the test, the file is removed from the merkyl tracker.

        Note that this is a blocking call, ie, we ensure that the file
        is being logged by merkyl, before we continue. This is important
        and prevents the file_add operation being queued and processes
        which generate log information activating before the log is being
        monitored. This is achieved using the grab_result switch, but
        in fact, nothing will be received.

        It is worth noting that the file path must be "discoverable" by merkyl.
        This may mean editing the allowed_files prior to deploying merkyl.

        Args:
            log_name: Full path to the log file wishing to be monitored.

        """
        fire_art_test_hook(
            self.node, 'add_log_merkyl', ip=self.ip,
            filename=log_name, grab_result=True)
Exemplo n.º 21
0
def pytest_exception_interact(node, call, report):
    from cfme.fixtures.pytest_store import store
    from six.moves.http_client import BadStatusLine
    from socket import error

    val = safe_string(call.excinfo.value)
    if isinstance(call.excinfo.value, (URLError, BadStatusLine, error)):
        logger.error("internal Exception:\n %s", str(call.excinfo))
        from cfme.utils.browser import manager
        manager.start()  # start will quit first and cycle wharf as well

    last_lines = "\n".join(report.longreprtext.split("\n")[-4:])

    short_tb = '{}\n{}\n{}'.format(
        last_lines, call.excinfo.type.__name__,
        val.encode('ascii', 'xmlcharrefreplace')
    )
    fire_art_test_hook(
        node, 'filedump',
        description="Traceback", contents=report.longreprtext, file_type="traceback",
        display_type="danger", display_glyph="align-justify", group_id="pytest-exception",
        slaveid=store.slaveid)
    fire_art_test_hook(
        node, 'filedump',
        description="Short traceback", contents=short_tb, file_type="short_tb",
        display_type="danger", display_glyph="align-justify", group_id="pytest-exception",
        slaveid=store.slaveid)
    exception_name = call.excinfo.type.__name__
    exception_lineno = call.excinfo.traceback[-1].lineno
    exception_filename = str(call.excinfo.traceback[-1].path).replace(
        project_path.strpath + "/", ''
    )
    exception_location = "{}:{}".format(exception_filename, exception_lineno)
    fire_art_test_hook(
        node, 'tb_info',
        exception=exception_name, file_line=exception_location,
        short_tb=short_tb, slave_id=store.slaveid
    )

    # base64 encoded to go into a data uri, same for screenshots
    tb = report.longreprtext
    if not isinstance(tb, six.binary_type):
        tb = tb.encode('utf-8')
    full_tb = base64.b64encode(tb)
    # errors are when exceptions are thrown outside of the test call phase
    report.when = getattr(report, 'when', 'setup')
    is_error = report.when != 'call'

    template_data = {
        'name': node.name,
        'file': node.fspath,
        'is_error': is_error,
        'fail_stage': report.when,
        'short_tb': short_tb,
        'full_tb': full_tb,
    }

    # Before trying to take a screenshot, we used to check if one of the browser_fixtures was
    # in this node's fixturenames, but that was too limited and preventing the capture of
    # screenshots. If removing that conditional now makes this too broad, we should consider
    # an isinstance(val, WebDriverException) check in addition to the browser fixture check that
    # exists here in commit 825ef50fd84a060b58d7e4dc316303a8b61b35d2

    screenshot = take_screenshot()
    template_data['screenshot'] = screenshot.png
    template_data['screenshot_error'] = screenshot.error
    if screenshot.png:
        fire_art_test_hook(
            node, 'filedump',
            description="Exception screenshot", file_type="screenshot", mode="wb",
            contents_base64=True, contents=template_data['screenshot'], display_glyph="camera",
            group_id="pytest-exception", slaveid=store.slaveid)
    if screenshot.error:
        fire_art_test_hook(
            node, 'filedump',
            description="Screenshot error", mode="w", contents_base64=False,
            contents=template_data['screenshot_error'], display_type="danger",
            group_id="pytest-exception", slaveid=store.slaveid)

    failed_test_tracking['tests'].append(template_data)
    if is_error:
        failed_test_tracking['total_errored'] += 1
    else:
        failed_test_tracking['total_failed'] += 1
Exemplo n.º 22
0
def pytest_exception_interact(node, call, report):
    from cfme.fixtures.pytest_store import store
    from http.client import BadStatusLine
    from socket import error
    val = safe_string(call.excinfo.value)
    if isinstance(call.excinfo.value, (URLError, BadStatusLine, error)):
        logger.error("internal Exception:\n %s", str(call.excinfo))
        from cfme.utils.browser import manager
        manager.start()  # start will quit first and cycle wharf as well

    last_lines = "\n".join(report.longreprtext.split("\n")[-4:])

    short_tb = '{}\n{}\n{}'.format(last_lines, call.excinfo.type.__name__,
                                   val.encode('ascii', 'xmlcharrefreplace'))
    fire_art_test_hook(node,
                       'filedump',
                       description="Traceback",
                       contents=report.longreprtext,
                       file_type="traceback",
                       display_type="danger",
                       display_glyph="align-justify",
                       group_id="pytest-exception",
                       slaveid=store.slaveid)
    fire_art_test_hook(node,
                       'filedump',
                       description="Short traceback",
                       contents=short_tb,
                       file_type="short_tb",
                       display_type="danger",
                       display_glyph="align-justify",
                       group_id="pytest-exception",
                       slaveid=store.slaveid)
    exception_name = call.excinfo.type.__name__
    exception_lineno = call.excinfo.traceback[-1].lineno
    exception_filename = str(call.excinfo.traceback[-1].path).replace(
        project_path.strpath + "/", '')
    exception_location = f"{exception_filename}:{exception_lineno}"
    fire_art_test_hook(node,
                       'tb_info',
                       exception=exception_name,
                       file_line=exception_location,
                       short_tb=short_tb,
                       slave_id=store.slaveid)

    # base64 encoded to go into a data uri, same for screenshots
    tb = report.longreprtext
    if not isinstance(tb, bytes):
        tb = tb.encode('utf-8')
    full_tb = base64.b64encode(tb)
    # errors are when exceptions are thrown outside of the test call phase
    report.when = getattr(report, 'when', 'setup')
    is_error = report.when != 'call'

    template_data = {
        'name': node.name,
        'file': node.fspath,
        'is_error': is_error,
        'fail_stage': report.when,
        'short_tb': short_tb,
        'full_tb': full_tb,
    }

    # Before trying to take a screenshot, we used to check if one of the browser_fixtures was
    # in this node's fixturenames, but that was too limited and preventing the capture of
    # screenshots. If removing that conditional now makes this too broad, we should consider
    # an isinstance(val, WebDriverException) check in addition to the browser fixture check that
    # exists here in commit 825ef50fd84a060b58d7e4dc316303a8b61b35d2

    screenshot = take_screenshot()
    template_data['screenshot'] = screenshot.png
    template_data['screenshot_error'] = screenshot.error
    if screenshot.png:
        fire_art_test_hook(node,
                           'filedump',
                           description="Exception screenshot",
                           file_type="screenshot",
                           mode="wb",
                           contents_base64=True,
                           contents=template_data['screenshot'],
                           display_glyph="camera",
                           group_id="pytest-exception",
                           slaveid=store.slaveid)
    if screenshot.error:
        fire_art_test_hook(node,
                           'filedump',
                           description="Screenshot error",
                           mode="w",
                           contents_base64=False,
                           contents=template_data['screenshot_error'],
                           display_type="danger",
                           group_id="pytest-exception",
                           slaveid=store.slaveid)

    failed_test_tracking['tests'].append(template_data)
    if is_error:
        failed_test_tracking['total_errored'] += 1
    else:
        failed_test_tracking['total_failed'] += 1