towncrier/src/towncrier/test/test_build.py

968 lines
29 KiB
Python
Raw Normal View History

# Copyright (c) Amber Brown, 2015
# See LICENSE for details.
2018-06-12 15:53:15 +02:00
import os
from subprocess import call
from textwrap import dedent
from click.testing import CliRunner
from twisted.trial.unittest import TestCase
from .._shell import cli
from ..build import _main
def setup_simple_project():
2018-06-12 15:53:15 +02:00
with open("pyproject.toml", "w") as f:
f.write("[tool.towncrier]\n" 'package = "foo"\n')
os.mkdir("foo")
with open("foo/__init__.py", "w") as f:
f.write('__version__ = "1.2.3"\n')
2018-06-12 15:53:15 +02:00
os.mkdir("foo/newsfragments")
class TestCli(TestCase):
2016-12-07 11:31:03 +01:00
maxDiff = None
def _test_command(self, command):
2016-12-07 11:31:03 +01:00
runner = CliRunner()
with runner.isolated_filesystem():
setup_simple_project()
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
# Towncrier treats this as 124.feature, ignoring .rst extension
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/124.feature.rst", "w") as f:
f.write("Extends levitation")
# Towncrier supports non-numeric newsfragment names.
with open("foo/newsfragments/baz.feature.rst", "w") as f:
f.write("Baz levitation")
# Towncrier supports files that have a dot in the name of the
# newsfragment
with open("foo/newsfragments/fix-1.2.feature", "w") as f:
f.write("Baz fix levitation")
# Towncrier ignores files that don't have a dot
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/README", "w") as f:
f.write("Blah blah")
# And files that don't have a valid category
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/README.rst", "w") as f:
f.write("**Blah blah**")
2016-12-07 11:31:03 +01:00
result = runner.invoke(command, ["--draft", "--date", "01-01-2001"])
2016-12-07 11:31:03 +01:00
self.assertEqual(0, result.exit_code)
self.assertEqual(
result.output,
2019-08-11 19:54:52 +02:00
dedent(
"""\
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
Foo 1.2.3 (01-01-2001)
======================
Features
--------
- Baz levitation (baz)
- Baz fix levitation (#2)
- Adds levitation (#123)
- Extends levitation (#124)
2019-08-11 19:54:52 +02:00
"""
),
2016-12-07 11:31:03 +01:00
)
def test_command(self):
self._test_command(cli)
def test_subcommand(self):
self._test_command(_main)
def test_no_newsfragment_directory(self):
"""
A missing newsfragment directory acts as if there are no changes.
"""
runner = CliRunner()
with runner.isolated_filesystem():
setup_simple_project()
os.rmdir("foo/newsfragments")
result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"])
self.assertEqual(1, result.exit_code, result.output)
self.assertIn("Failed to list the news fragment files.\n", result.output)
def test_no_newsfragments(self):
"""
An empty newsfragment directory acts as if there are no changes.
"""
runner = CliRunner()
with runner.isolated_filesystem():
setup_simple_project()
result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"])
self.assertEqual(0, result.exit_code)
self.assertIn("No significant changes.\n", result.output)
def test_collision(self):
runner = CliRunner()
with runner.isolated_filesystem():
setup_simple_project()
# Note that both are 123.feature
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
with open("foo/newsfragments/123.feature.rst", "w") as f:
f.write("Extends levitation")
2018-06-12 15:53:15 +02:00
result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"])
# This should fail
self.assertEqual(type(result.exception), ValueError)
self.assertIn("multiple files for 123.feature", str(result.exception))
def test_section_and_type_sorting(self):
"""
Sections and types should be output in the same order that they're
defined in the config file.
"""
runner = CliRunner()
def run_order_scenario(sections, types):
with runner.isolated_filesystem():
2018-06-12 15:53:15 +02:00
with open("pyproject.toml", "w") as f:
f.write(
dedent(
"""
[tool.towncrier]
package = "foo"
directory = "news"
2018-06-12 15:53:15 +02:00
"""
)
)
for section in sections:
2018-06-12 15:53:15 +02:00
f.write(
dedent(
"""
[[tool.towncrier.section]]
path = "{section}"
name = "{section}"
2018-06-12 15:53:15 +02:00
""".format(
section=section
)
)
)
for type_ in types:
2018-06-12 15:53:15 +02:00
f.write(
dedent(
"""
[[tool.towncrier.type]]
directory = "{type_}"
name = "{type_}"
showcontent = true
2018-06-12 15:53:15 +02:00
""".format(
type_=type_
)
)
)
os.mkdir("foo")
with open("foo/__init__.py", "w") as f:
f.write('__version__ = "1.2.3"\n')
2018-06-12 15:53:15 +02:00
os.mkdir("news")
for section in sections:
sectdir = "news/" + section
os.mkdir(sectdir)
for type_ in types:
with open(f"{sectdir}/1.{type_}", "w") as f:
f.write(f"{section} {type_}")
return runner.invoke(
2018-06-12 15:53:15 +02:00
_main, ["--draft", "--date", "01-01-2001"], catch_exceptions=False
)
2018-06-12 15:53:15 +02:00
result = run_order_scenario(["section-a", "section-b"], ["type-1", "type-2"])
self.assertEqual(0, result.exit_code)
self.assertEqual(
result.output,
"Loading template...\nFinding news fragments...\nRendering news "
"fragments...\nDraft only -- nothing has been written.\nWhat is "
"seen below is what would be written.\n\nFoo 1.2.3 (01-01-2001)"
"\n======================"
2018-06-12 15:53:15 +02:00
+ dedent(
"""
section-a
---------
type-1
~~~~~~
- section-a type-1 (#1)
type-2
~~~~~~
- section-a type-2 (#1)
section-b
---------
type-1
~~~~~~
- section-b type-1 (#1)
type-2
~~~~~~
- section-b type-2 (#1)
2018-06-12 15:53:15 +02:00
"""
),
)
2018-06-12 15:53:15 +02:00
result = run_order_scenario(["section-b", "section-a"], ["type-2", "type-1"])
2016-12-07 11:31:03 +01:00
self.assertEqual(0, result.exit_code)
self.assertEqual(
result.output,
"Loading template...\nFinding news fragments...\nRendering news "
"fragments...\nDraft only -- nothing has been written.\nWhat is "
"seen below is what would be written.\n\nFoo 1.2.3 (01-01-2001)"
"\n======================"
2018-06-12 15:53:15 +02:00
+ dedent(
"""
section-b
---------
type-2
~~~~~~
- section-b type-2 (#1)
type-1
~~~~~~
- section-b type-1 (#1)
section-a
---------
type-2
~~~~~~
- section-a type-2 (#1)
type-1
~~~~~~
- section-a type-1 (#1)
2018-06-12 15:53:15 +02:00
"""
),
2016-12-07 11:31:03 +01:00
)
def test_no_confirmation(self):
runner = CliRunner()
with runner.isolated_filesystem():
setup_simple_project()
2018-06-12 15:53:15 +02:00
fragment_path1 = "foo/newsfragments/123.feature"
fragment_path2 = "foo/newsfragments/124.feature.rst"
with open(fragment_path1, "w") as f:
f.write("Adds levitation")
with open(fragment_path2, "w") as f:
f.write("Extends levitation")
call(["git", "init"])
call(["git", "config", "user.name", "user"])
call(["git", "config", "user.email", "user@example.com"])
call(["git", "add", "."])
call(["git", "commit", "-m", "Initial Commit"])
2018-06-12 15:53:15 +02:00
result = runner.invoke(_main, ["--date", "01-01-2001", "--yes"])
self.assertEqual(0, result.exit_code)
2018-06-12 15:53:15 +02:00
path = "NEWS.rst"
self.assertTrue(os.path.isfile(path))
self.assertFalse(os.path.isfile(fragment_path1))
self.assertFalse(os.path.isfile(fragment_path2))
2018-03-26 15:25:47 +02:00
def test_needs_config(self):
"""
Towncrier needs a configuration file.
"""
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(_main, ["--draft"])
self.assertEqual(1, result.exit_code, result.output)
self.assertTrue(result.output.startswith("No configuration file found."))
2018-03-26 15:25:47 +02:00
def test_projectless_changelog(self):
"""In which a directory containing news files is built into a changelog
- without a Python project or version number. We override the
project title from the commandline.
"""
runner = CliRunner()
with runner.isolated_filesystem():
2018-06-12 15:53:15 +02:00
with open("pyproject.toml", "w") as f:
f.write("[tool.towncrier]\n" 'package = "foo"\n')
os.mkdir("foo")
os.mkdir("foo/newsfragments")
with open("foo/newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
2018-03-26 15:25:47 +02:00
# Towncrier ignores .rst extension
2018-06-12 15:53:15 +02:00
with open("foo/newsfragments/124.feature.rst", "w") as f:
f.write("Extends levitation")
result = runner.invoke(
_main,
[
"--name",
"FooBarBaz",
"--version",
"7.8.9",
"--date",
"01-01-2001",
"--draft",
],
)
2018-03-26 15:25:47 +02:00
self.assertEqual(0, result.exit_code)
self.assertEqual(
result.output,
2018-06-12 15:53:15 +02:00
dedent(
"""
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
FooBarBaz 7.8.9 (01-01-2001)
============================
Features
--------
- Adds levitation (#123)
- Extends levitation (#124)
2018-06-12 15:53:15 +02:00
"""
).lstrip(),
2018-03-26 15:25:47 +02:00
)
def test_version_in_config(self):
"""The calling towncrier with version defined in configfile.
Specifying a version in toml file will be helpful if version
is maintained by i.e. bumpversion and it's not a python project.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write("[tool.towncrier]\n" 'version = "7.8.9"\n')
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
result = runner.invoke(_main, ["--date", "01-01-2001", "--draft"])
self.assertEqual(0, result.exit_code, result.output)
self.assertEqual(
result.output,
dedent(
"""
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
7.8.9 (01-01-2001)
==================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)
def test_project_name_in_config(self):
"""The calling towncrier with project name defined in configfile.
Specifying a project name in toml file will be helpful to keep the
project name consistent as part of the towncrier configuration, not call.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write("[tool.towncrier]\n" 'name = "ImGoProject"\n')
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
result = runner.invoke(
_main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"]
)
self.assertEqual(0, result.exit_code, result.output)
self.assertEqual(
result.output,
dedent(
"""
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
ImGoProject 7.8.9 (01-01-2001)
==============================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)
def test_no_package_changelog(self):
"""The calling towncrier with any package argument.
Specifying a package in the toml file or the command line
should not always be needed:
- we can set the version number on the command line,
so we do not need the package for that.
- we don't need to include the package in the changelog header.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write("[tool.towncrier]")
2018-06-12 15:53:15 +02:00
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
2018-06-12 15:53:15 +02:00
result = runner.invoke(
_main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"]
)
self.assertEqual(0, result.exit_code, result.output)
self.assertEqual(
result.output,
2018-06-12 15:53:15 +02:00
dedent(
"""
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
7.8.9 (01-01-2001)
==================
Features
--------
- Adds levitation (#123)
2018-06-12 15:53:15 +02:00
"""
).lstrip(),
)
def test_single_file(self):
"""
Enabling the single file mode will write the changelog to a filename
that is formatted from the filename args.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
'[tool.towncrier]\n single_file=true\n filename="{version}-notes.rst"'
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)
self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(os.path.exists("7.8.9-notes.rst"), os.listdir("."))
with open("7.8.9-notes.rst") as f:
output = f.read()
self.assertEqual(
output,
dedent(
"""
foo 7.8.9 (01-01-2001)
======================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)
def test_singlefile_errors_and_explains_cleanly(self):
"""
Failure to find the configuration file results in a clean explanation
without a traceback.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write('[tool.towncrier]\n singlefile="fail!"\n')
result = runner.invoke(_main)
self.assertEqual(1, result.exit_code)
self.assertEqual(
"`singlefile` is not a valid option. Did you mean `single_file`?\n",
result.output,
)
def test_single_file_false(self):
"""
If formatting arguments are given in the filename arg and single_file is
false, the filename will not be formatted.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
'[tool.towncrier]\n single_file=false\n filename="{version}-notes.rst"'
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)
self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(os.path.exists("{version}-notes.rst"), os.listdir("."))
self.assertFalse(os.path.exists("7.8.9-notes.rst"), os.listdir("."))
with open("{version}-notes.rst") as f:
output = f.read()
self.assertEqual(
output,
dedent(
"""
foo 7.8.9 (01-01-2001)
======================
Features
--------
- Adds levitation (#123)
"""
).lstrip(),
)
def test_bullet_points_false(self):
"""
When all_bullets is false, subsequent lines are not indented.
2022-04-10 17:36:14 +02:00
The automatic ticket number inserted by towcier will allign with the
manual bullet.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
"[tool.towncrier]\n"
'template="towncrier:single-file-no-bullets"\n'
"all_bullets=false"
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("wow!\n" "~~~~\n" "\n" "No indentation at all.")
2022-04-10 17:36:14 +02:00
with open("newsfragments/124.bugfix", "w") as f:
f.write("#. Numbered bullet list.")
2022-04-10 17:36:14 +02:00
with open("newsfragments/125.removal", "w") as f:
f.write("- Hyphen based bullet list.")
2022-04-10 17:36:14 +02:00
with open("newsfragments/126.doc", "w") as f:
f.write("* Asterisk based bullet list.")
result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)
self.assertEqual(0, result.exit_code, result.output)
with open("NEWS.rst") as f:
output = f.read()
self.assertEqual(
output,
2022-04-10 17:36:14 +02:00
"""
foo 7.8.9 (01-01-2001)
======================
2022-04-10 17:36:14 +02:00
Features
--------
2022-04-10 17:36:14 +02:00
wow!
~~~~
2022-04-10 17:36:14 +02:00
No indentation at all.
(#123)
Bugfixes
--------
#. Numbered bullet list.
(#124)
Improved Documentation
----------------------
* Asterisk based bullet list.
(#126)
Deprecations and Removals
-------------------------
- Hyphen based bullet list.
(#125)
""".lstrip(),
)
def test_title_format_custom(self):
"""
A non-empty title format adds the specified title.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
dedent(
"""\
[tool.towncrier]
package = "foo"
title_format = "[{project_date}] CUSTOM RELEASE for {name} version {version}"
"""
)
)
os.mkdir("foo")
os.mkdir("foo/newsfragments")
with open("foo/newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
# Towncrier ignores .rst extension
with open("foo/newsfragments/124.feature.rst", "w") as f:
f.write("Extends levitation")
result = runner.invoke(
_main,
[
"--name",
"FooBarBaz",
"--version",
"7.8.9",
"--date",
"20-01-2001",
"--draft",
],
)
expected_output = dedent(
"""\
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
[20-01-2001] CUSTOM RELEASE for FooBarBaz version 7.8.9
=======================================================
Features
--------
- Adds levitation (#123)
- Extends levitation (#124)
"""
)
self.assertEqual(0, result.exit_code)
self.assertEqual(expected_output, result.output)
def test_title_format_false(self):
"""
Setting the title format to false disables the explicit title. This
would be used, for example, when the template creates the title itself.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
dedent(
"""\
[tool.towncrier]
package = "foo"
title_format = false
2020-12-21 19:56:10 +01:00
template = "template.rst"
"""
)
)
os.mkdir("foo")
os.mkdir("foo/newsfragments")
2020-12-21 19:56:10 +01:00
with open("template.rst", "w") as f:
f.write(
dedent(
"""\
2020-12-21 19:56:10 +01:00
Here's a hardcoded title added by the template
==============================================
{% for section in sections %}
{% set underline = "-" %}
{% for category, val in definitions.items() if category in sections[section] %}
{% for text, values in sections[section][category]|dictsort(by='value') %}
- {{ text }}
{% endfor %}
{% endfor %}
{% endfor %}
"""
)
)
result = runner.invoke(
_main,
[
"--name",
"FooBarBaz",
"--version",
"7.8.9",
"--date",
"20-01-2001",
"--draft",
],
)
expected_output = dedent(
"""\
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.
2020-12-21 19:56:10 +01:00
Here's a hardcoded title added by the template
==============================================
"""
)
self.assertEqual(0, result.exit_code)
self.assertEqual(expected_output, result.output)
def test_start_string(self):
"""
The `start_string` configuration is used to detect the starting point
for inserting the generated release notes. A newline is automatically
added to the configured value.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f:
f.write(
dedent(
"""\
[tool.towncrier]
start_string="Release notes start marker"
"""
)
)
os.mkdir("newsfragments")
with open("newsfragments/123.feature", "w") as f:
f.write("Adds levitation")
with open("NEWS.rst", "w") as f:
f.write("a line\n\nanother\n\nRelease notes start marker\n")
result = runner.invoke(
_main,
[
"--version",
"7.8.9",
"--name",
"foo",
"--date",
"01-01-2001",
"--yes",
],
)
self.assertEqual(0, result.exit_code, result.output)
self.assertTrue(os.path.exists("NEWS.rst"), os.listdir("."))
with open("NEWS.rst") as f:
output = f.read()
expected_output = dedent(
"""\
a line
another
Release notes start marker
foo 7.8.9 (01-01-2001)
======================
Features
--------
- Adds levitation (#123)
"""
)
self.assertEqual(expected_output, output)
2020-12-16 04:51:29 +01:00
def test_with_topline_and_template_and_draft(self):
"""
Spacing is proper when drafting with a topline and a template.
"""
runner = CliRunner()
with runner.isolated_filesystem():
with open("pyproject.toml", "w") as f: