<commit_before>#!/usr/bin/env python3.6

"""Synthesizes multiple libraries and reports status."""

import argparse
import functools
import importlib
import os
import pathlib
import requests
import subprocess
import sys
import typing
import jinja2
import yaml

from autosynth import executor, github, synth
from autosynth.log import logger


# Callable type to help dependency inject for testing
Runner = typing.Callable[
    [typing.List[str], typing.Any, pathlib.Path], typing.Tuple[int, bytes]
]


def _execute(
    command: typing.List[str], env: typing.Any, log_file_path: pathlib.Path
) -> typing.Tuple[int, bytes]:
    """Helper to wrap command invocation for testing"""
    # Ensure the logfile directory exists
    log_file_path.parent.mkdir(parents=True, exist_ok=True)
    with open(log_file_path, "wb+") as log_file:
        result = executor.run(
            command=command,
            stdout=log_file,
            stderr=subprocess.STDOUT,
            check=False,
            encoding="utf-8",
            env=env,
        )
    with open(log_file_path, "rb") as fp:
        return (result.returncode, fp.read())


def synthesize_library(
    library: typing.Dict,
    github_token: str,
    extra_args: typing.List[str],
    base_log_path: pathlib.Path,
    runner: Runner = _execute,
) -> typing.Dict:
    """Run autosynth on a single library.

    Arguments:
        library {dict} - Library configuration

    """
    logger.info(f"Synthesizing {library['name']}.")

    command = [sys.executable, "-m", "autosynth.synth"]

    env = os.environ
    env["GITHUB_TOKEN"] = github_token

    library_args = [
        "--repository",
        library["repository"],
        "--synth-path",
        library.get("synth-path", ""),
        "--branch-suffix",
        library.get("branch-suffix", ""),
        "--pr-title",
        library.get("pr-title", ""),
    ]

    if library.get("metadata-path"):
        library_args.extend(["--metadata-path", library.get("metadata-path")])

    if library.get("deprecated-execution", False):
        library_args.append("--deprecated-execution")

    log_file_path = base_log_path / library["repository"] / "sponge_log.log"
    # run autosynth in a separate process
    (returncode, output) = runner(
        command + library_args + library.get("args", []) + extra_args,
        env,
        log_file_path,
    )
    error = returncode not in (0, synth.EXIT_CODE_SKIPPED)
    skipped = returncode == synth.EXIT_CODE_SKIPPED
    if error:
        logger.error(f"Synthesis failed for {library['name']}")
    return {
        "name": library["name"],
        "output": output.decode("utf-8"),
        "error": error,
        "skipped": skipped,
    }


def make_report(name: str, results: typing.List[typing.Dict]) -> None:
    """Write an xunit report sponge_log.xml to the current directory.

    Arguments:
        name {str} - Name of the report
        results {typing.List[typing.Dict]} - List of synth results
    """
    with open("report.xml.j2") as fh:
        template = jinja2.Template(fh.read())

    output = template.render(
        name=name,
        failures=len([result for result in results if result["error"]]),
        skips=len([result for result in results if result["skipped"]]),
        results=results,
    )

    with open("sponge_log.xml", "w") as fh:
        fh.write(output)


@functools.lru_cache()
def _list_issues_cached(gh, *args, **kwargs):
    """A caching wrapper for listing issues, so we don't expend our quota."""
    return list(gh.list_issues(*args, **kwargs))


def _close_issue(gh, repository: str, existing_issue: dict):
    if existing_issue is None:
        return

    logger.info(f"Closing issue: {existing_issue['url']}")
    gh.create_issue_comment(
        repository,
        issue_number=existing_issue["number"],
        comment="Autosynth passed, closing! :green_heart:",
    )
    gh.patch_issue(
        repository, issue_number=existing_issue["number"], state="closed",
    )


def _file_or_comment_on_issue(
    gh, name, repository, issue_title, existing_issue, output
):
    # GitHub rejects issues with bodies > 64k
    output_to_report = output[-10000:]
    message = f"""\
Here's the output from running `synth.py`:

```
{output_to_report}
```

Google internal developers can see the full log [here](http://sponge/{os.environ.get('KOKORO_BUILD_ID')}).
"""

    if not existing_issue:
        issue_details = (
            f"Hello! Autosynth couldn't regenerate {name}. :broken_heart:\n\n{message}"
        )
        labels = ["autosynth failure", "priority: p1", "type: bug"]

        api_label = gh.get_api_label(repository, name)
        if api_label:
            labels.append(api_label)

        issue = gh.create_issue(
            repository, title=issue_title, body=issue_details, labels=labels,
        )<commit_msg>fix: deep link into sponge2 which should contain the full output (#610)<commit_after>#!/usr/bin/env python3.6

"""Synthesizes multiple libraries and reports status."""

import argparse
import functools
import importlib
import os
import pathlib
import requests
import subprocess
import sys
import typing
import jinja2
import yaml

from autosynth import executor, github, synth
from autosynth.log import logger


# Callable type to help dependency inject for testing
Runner = typing.Callable[
    [typing.List[str], typing.Any, pathlib.Path], typing.Tuple[int, bytes]
]


def _execute(
    command: typing.List[str], env: typing.Any, log_file_path: pathlib.Path
) -> typing.Tuple[int, bytes]:
    """Helper to wrap command invocation for testing"""
    # Ensure the logfile directory exists
    log_file_path.parent.mkdir(parents=True, exist_ok=True)
    with open(log_file_path, "wb+") as log_file:
        result = executor.run(
            command=command,
            stdout=log_file,
            stderr=subprocess.STDOUT,
            check=False,
            encoding="utf-8",
            env=env,
        )
    with open(log_file_path, "rb") as fp:
        return (result.returncode, fp.read())


def synthesize_library(
    library: typing.Dict,
    github_token: str,
    extra_args: typing.List[str],
    base_log_path: pathlib.Path,
    runner: Runner = _execute,
) -> typing.Dict:
    """Run autosynth on a single library.

    Arguments:
        library {dict} - Library configuration

    """
    logger.info(f"Synthesizing {library['name']}.")

    command = [sys.executable, "-m", "autosynth.synth"]

    env = os.environ
    env["GITHUB_TOKEN"] = github_token

    library_args = [
        "--repository",
        library["repository"],
        "--synth-path",
        library.get("synth-path", ""),
        "--branch-suffix",
        library.get("branch-suffix", ""),
        "--pr-title",
        library.get("pr-title", ""),
    ]

    if library.get("metadata-path"):
        library_args.extend(["--metadata-path", library.get("metadata-path")])

    if library.get("deprecated-execution", False):
        library_args.append("--deprecated-execution")

    log_file_path = base_log_path / library["repository"] / "sponge_log.log"
    # run autosynth in a separate process
    (returncode, output) = runner(
        command + library_args + library.get("args", []) + extra_args,
        env,
        log_file_path,
    )
    error = returncode not in (0, synth.EXIT_CODE_SKIPPED)
    skipped = returncode == synth.EXIT_CODE_SKIPPED
    if error:
        logger.error(f"Synthesis failed for {library['name']}")
    return {
        "name": library["name"],
        "output": output.decode("utf-8"),
        "error": error,
        "skipped": skipped,
    }


def make_report(name: str, results: typing.List[typing.Dict]) -> None:
    """Write an xunit report sponge_log.xml to the current directory.

    Arguments:
        name {str} - Name of the report
        results {typing.List[typing.Dict]} - List of synth results
    """
    with open("report.xml.j2") as fh:
        template = jinja2.Template(fh.read())

    output = template.render(
        name=name,
        failures=len([result for result in results if result["error"]]),
        skips=len([result for result in results if result["skipped"]]),
        results=results,
    )

    with open("sponge_log.xml", "w") as fh:
        fh.write(output)


@functools.lru_cache()
def _list_issues_cached(gh, *args, **kwargs):
    """A caching wrapper for listing issues, so we don't expend our quota."""
    return list(gh.list_issues(*args, **kwargs))


def _close_issue(gh, repository: str, existing_issue: dict):
    if existing_issue is None:
        return

    logger.info(f"Closing issue: {existing_issue['url']}")
    gh.create_issue_comment(
        repository,
        issue_number=existing_issue["number"],
        comment="Autosynth passed, closing! :green_heart:",
    )
    gh.patch_issue(
        repository, issue_number=existing_issue["number"], state="closed",
    )


def _get_sponge_log_url(repository: str) -> str:
    invocation_id = os.environ.get("KOKORO_BUILD_ID")
    target = repository.split("/")[-1]
    return f"http://sponge2/results/invocations/{invocation_id}/targets/github%2Fsynthtool;config=default/tests;query={target};failed=false"


def _file_or_comment_on_issue(
    gh, name, repository, issue_title, existing_issue, output
):
    # GitHub rejects issues with bodies > 64k
    output_to_report = output[-10000:]
    sponge_log_url = _get_sponge_log_url(repository)
    message = f"""\
Here's the output from running `synth.py`:

```
{output_to_report}
```

Google internal developers can see the full log [here]({sponge_log_url}).
"""

    if not existing_issue:
        issue_details = (
            f"Hello! Autosynth couldn't regenerate {name}. :broken_heart:\n\n{message}"
        )
        labels = ["autosynth failure", "priority: p1", "type: bug"]

        api_label = gh.get_api_label(repository, name)
        if api_label:
            labels.append(api_label)

        issue = gh.create_issue(
            repository, title=issue_title, body=issue_details, labels=labels,
        )
{'fraction_non_alphanumeric': 0.1287946650741515, 'fraction_numerical': 0.003881755747984473, 'mean_word_length': 3.0272545090180363, 'pattern_counts': {'":': 8, '<': 3, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '721072', 'n_tokens_mistral': 3239, 'n_tokens_neox': 3088, 'n_words': 745}