Skip to content
This repository has been archived by the owner on Feb 23, 2020. It is now read-only.

Commit

Permalink
#340 Improve Tags tests (#387)
Browse files Browse the repository at this point in the history
* #340  Rm code doubling with BaseCatalogTestCase class

* #340  Create selenium test for products count
  • Loading branch information
duker33 committed Dec 25, 2018
1 parent e5af29e commit b3f0fcb
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 76 deletions.
27 changes: 26 additions & 1 deletion stroyprombeton/tests/tests_selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ def click_load_more_button(self):
lambda browser: self.get_tables_rows_count(browser) > count
)

def get_loaded_products_count(self):
return (
self.browser
.find_element_by_id(self.LOAD_MORE_ID)
.get_attribute('data-total-products')
)

def is_load_more_disabled(self, browser=None):
browser = browser or self.browser
return 'disabled' in (
Expand Down Expand Up @@ -445,7 +452,7 @@ def wait_filter(browser):

self.assertGreater(before_filter_products, after_filter_products)

def test_filter_by_tag_button(self):
def test_tag_button_change_url(self):
"""Filter button with the filled one of tag checkboxes should change url to tag."""
# set one product with no tags in test_db
# check if this prod is not in list after filtering
Expand All @@ -462,6 +469,24 @@ def test_filter_by_tag_button(self):
self.assertIn(tagged_category_path, self.browser.current_url)
self.browser.find_element_by_class_name('js-clear-tag-filter').click()

def test_tag_button_filter_products(self):
# this category contains 25 tags. It's less then products on page limit.
category = stb_models.Category.objects.get(name='Category #1 of #2')
tag_slug = '1-m'
tag_selector = self.FILTER_TAG_TEMPLATE.format(tag_slug=tag_slug)
self.load_category_page(category)
before_products_count = self.get_loaded_products_count()

self.browser.find_element_by_css_selector(tag_selector).click()
self.apply_tags()

after_products_count = self.get_loaded_products_count()
self.assertGreater(int(before_products_count), int(after_products_count))

self.browser.find_element_by_class_name('js-clear-tag-filter').click()
after_products_count = self.get_loaded_products_count()
self.assertEqual(int(before_products_count), int(after_products_count))

def test_flush_button(self):
category = self.root_category
self.load_category_page(
Expand Down
132 changes: 58 additions & 74 deletions stroyprombeton/tests/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,40 @@ def json_to_dict(response: HttpResponse) -> dict():
return json.loads(response.content)


class BaseCatalogTestCase(TestCase):

fixtures = ['dump.json']

def setUp(self):
self.root_category = models.Category.objects.filter(parent=None).first()
self.category = models.Category.objects.root_nodes().select_related('page').first()
self.tags = models.Tag.objects.order_by(*settings.TAGS_ORDER).all()

def get_category_url(
self,
category: models.Category = None,
tags: models.TagQuerySet = None,
sorting: int = None,
query_string: dict = None,
):
category = category or self.category
return reverse_catalog_url(
'category', {'category_id': category.id}, tags, sorting, query_string,
)

def get_category_page(self, *args, **kwargs):
return self.client.get(self.get_category_url(*args, **kwargs))

def get_category_soup(self, *args, **kwargs) -> BeautifulSoup:
category_page = self.get_category_page(*args, **kwargs)
return BeautifulSoup(
category_page.content.decode('utf-8'),
'html.parser'
)


# @todo #340:60m Move TestPageMixin to some PageData class.
# And remove CategoryTable().response field.
class TestPageMixin:

@property
Expand Down Expand Up @@ -128,7 +162,7 @@ def test_children_category_name(self):


@tag('fast')
class CategoryTable(TestCase, TestPageMixin):
class CategoryTable(BaseCatalogTestCase, TestPageMixin):
"""
Test for CategoryPage view.
Expand All @@ -139,15 +173,15 @@ class CategoryTable(TestCase, TestPageMixin):

def setUp(self):
"""Create category and product."""
super().setUp()

category_data = {
'name': 'Test root category',
'page': ModelPage.objects.create(
name='Козырьки',
content='Козырьки устанавливают над входами зданий.',
)
}

self.root_category = models.Category.objects.filter(parent=None).first()
self.category = models.Category.objects.create(**category_data)

self.data = {
Expand All @@ -161,39 +195,14 @@ def setUp(self):
content='Козырьки устанавливают над входами зданий.',
)
}

models.Product.objects.create(**self.data)

self.response = self.client.get(self.get_category_url())

def get_category_url(
self,
category: models.Category = None,
tags: models.TagQuerySet = None,
sorting: int = None,
query_string: dict = None,
):
category = category or self.category
return reverse_catalog_url(
'category', {'category_id': category.id}, tags, sorting, query_string,
)

@property
def response_product(self):
return self.response.context['products'][0]

def get_category_page( # Ignore CPDBear
self,
category: models.Category=None,
tags: models.TagQuerySet=None,
sorting: int=None,
query_string: dict=None,
):
category = category or self.category
return self.client.get(reverse_catalog_url(
'category', {'category_id': category.id}, tags, sorting, query_string,
))

def test_products_quantity(self):
self.assertEqual(len(self.response.context['products']), 1)

Expand Down Expand Up @@ -223,25 +232,6 @@ def test_inactive_product_not_in_category(self):
response = self.client.get(reverse('category', args=(test_product.category_id,)))
self.assertNotIn(test_product, response.context['products'])

# @todo #320:60m Take CatalogTags tests from SE.
# `shopelectro.tests.tests_views.CatalogTags`
def test_filter_products_by_tags(self):
"""Category page should not contain products, excluded by tags selection."""
tag_slug = '2-m'
tag_qs = models.Tag.objects.filter(slug=tag_slug)
tag = tag_qs.first()
response = self.get_category_page(category=self.root_category, tags=tag_qs)

# find product: it has no tag and it's descendant of root_category
disappeared_products = (
models.Product.objects.active()
.get_category_descendants(self.root_category)
.exclude(tags=tag)
)
self.assertFalse(
any(p.name in response.content.decode() for p in disappeared_products)
)

def test_load_more_context_data(self):
"""App should response with products data on load_more request."""
db_products = models.Product.objects.active().get_category_descendants(
Expand Down Expand Up @@ -601,33 +591,10 @@ def test_price_list(self):


@tag('fast')
class CatalogTags(TestCase, CategoryTestMixin):
class CatalogTags(BaseCatalogTestCase, CategoryTestMixin):

fixtures = ['dump.json']

def setUp(self):
self.category = models.Category.objects.root_nodes().select_related('page').first()
self.tags = models.Tag.objects.order_by(*settings.TAGS_ORDER).all()

def get_category_page(
self,
category: models.Category=None,
tags: models.TagQuerySet=None,
sorting: int=None,
query_string: dict=None,
):
category = category or self.category
return self.client.get(reverse_catalog_url(
'category', {'category_id': category.id}, tags, query_string,
))

def get_category_soup(self, *args, **kwargs) -> BeautifulSoup:
category_page = self.get_category_page(*args, **kwargs)
return BeautifulSoup(
category_page.content.decode('utf-8'),
'html.parser'
)

def test_category_page_contains_all_tags(self):
"""Category contains all Product's tags."""
response = self.client.get(self.get_category_path())
Expand Down Expand Up @@ -721,12 +688,12 @@ def test_tag_titles_content_conjunction(self):
tag_titles = delimiter.join(t.name for t in tags)
self.assertContains(response, tag_titles)

def test_tags_var(self):
def test_tags_var_in_db_template(self):
"""
Test CategoryTagsPage with canonical tags.
CategoryTagsPage should contain "tags" template var tag=each(tags) is Tag
class instance.
"tags" db template at CategoryTagsPage
should render tag names. For example "1 м, 20 кг".
"""
tags = models.Tag.objects.order_by(*settings.TAGS_ORDER).all()
response = self.client.get(self.get_category_path(tags=tags))
Expand Down Expand Up @@ -805,3 +772,20 @@ def test_too_many_tags_collapse(self):
# @todo #374:30m Create test for from_index, to_index correctness.
# Dangerous case: ['1 м', '2 м', '11 м'] -> 'от 1 м до 2 м'
self.assertIn(f'от {from_index} м до {to_index - 1} м', tags_text)

def test_filter_products_by_tags(self):
"""Category page should not contain products, excluded by tags selection."""
tag_slug = '2-m'
tag_qs = models.Tag.objects.filter(slug=tag_slug)
tag = tag_qs.first()
response = self.get_category_page(category=self.root_category, tags=tag_qs)

# find product: it has no tag and it's descendant of root_category
disappeared_products = (
models.Product.objects.active()
.get_category_descendants(self.root_category)
.exclude(tags=tag)
)
self.assertFalse(
any(p.name in response.content.decode() for p in disappeared_products)
)
2 changes: 1 addition & 1 deletion templates/catalog/category.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h2 class="center-xs product-list-title">Товары в категории</h2>
</tbody>
</table>

{% include 'components/show_more_products.html' with category=category only %}
{% include 'components/show_more_products.html' with category=category total_products=total_products only %}
{% include 'catalog/seo_links.html' with page_obj=page_obj paginator_links=paginator_links pagination_param=pagination_param only %}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions templates/components/show_more_products.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
data-category="{{ category.id }}"
data-url="{{ category.url }}"
data-load-count="30"
data-total-products="{{ total_products }}"
>
Показать еще
<div class="show-more-arrow
Expand Down

1 comment on commit b3f0fcb

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on b3f0fcb Dec 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to retrieve PDD puzzles from the code base and submit them to GitHub. If you think that it's a bug on our side, please submit it to yegor256/0pdd:

set -x && set -e && set -o pipefail && cd /tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton && pdd -v -f /tmp/20181225-978-159ebt2 [1]: bash: warning: setlocale: LC_ALL: cannot change locale (UTF-8) + set -e + set -o pipefail + cd /tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton + pdd -v -f...

Please, copy and paste this stack trace to GitHub:

UserError
set -x && set -e && set -o pipefail && cd /tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton && pdd -v -f /tmp/20181225-978-159ebt2 [1]:
bash: warning: setlocale: LC_ALL: cannot change locale (UTF-8)
+ set -e
+ set -o pipefail
+ cd /tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton
+ pdd -v -f /tmp/20181225-978-159ebt2

My version is 0.20.4
Ruby version is 2.5.1 at x86_64-linux
Reading /tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton
310 file(s) found, 56 excluded
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/management/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/settings/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/templatetags/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/migrations/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/locale/ru/LC_MESSAGES/django.mo is a binary file (7319 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/tests/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/tests/assets/review.jpg is a binary file (149793 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/formats/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/formats/ru/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/stroyprombeton/formats/en/__init__.py is a binary file (0 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/header-image.jpg is a binary file (56247 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/favicon.ico is a binary file (894 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/stk-logo.png is a binary file (2819 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/common/image-thumb.png is a binary file (6244 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/pages/arrow-down-hover.png is a binary file (303 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/pages/picture.png is a binary file (373 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/pages/arrow-down-active.png is a binary file (292 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/pages/arrow-down-disabled.png is a binary file (304 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/pages/arrow-down.png is a binary file (303 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-commercial-disable.png is a binary file (1199 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-pricelist.png is a binary file (1325 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-pricelist-hover.png is a binary file (1088 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-pricelist-disable.png is a binary file (1125 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-draft.png is a binary file (1688 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-commercial.png is a binary file (1217 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-draft-hover.png is a binary file (1580 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-draft-disable.png is a binary file (1613 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/spriteSrc/main/offer-commercial-hover.png is a binary file (1204 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/ajax-loader.gif is a binary file (4178 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/catalog-new-jersey.png is a binary file (51903 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/partner-stkmetall-logo.png is a binary file (2441 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/catalog-gbi.png is a binary file (57174 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/icon-photo.png is a binary file (373 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/photoSwipe/preloader.gif is a binary file (866 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/photoSwipe/default-skin.png is a binary file (547 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/stk-prombeton.png is a binary file (3414 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/partner-stroysila-logo.png is a binary file (5938 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/images/partner-stkmodul-logo.png is a binary file (2581 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/glyphicons-halflings-regular.woff2 is a binary file (18028 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/glyphicons-halflings-regular.woff is a binary file (23424 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/fontawesome-webfont.woff2 is a binary file (71896 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/slick.woff is a binary file (1380 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/slick.ttf is a binary file (1892 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/front/fonts/fontawesome-webfont.woff is a binary file (90412 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/categories/1/1.png is a binary file (7609 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/products/1/small.jpg is a binary file (91604 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/products/1/main.jpg is a binary file (91604 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/posts/Russia-map.png is a binary file (60128 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/index-page/01.png is a binary file (15572 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/news1-head.png is a binary file (977647 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/08-09-16/165x165.jpg is a binary file (25797 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/news1-2.png is a binary file (256741 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/thumb-news-1.png is a binary file (46818 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/thumb-b-news-2.png is a binary file (113417 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/thumb-b-news-1.png is a binary file (100807 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/media/news/news1-1.png is a binary file (191831 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/doc/images/table_editor/add_product_button.png is a binary file (22893 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/doc/images/table_editor/filters_group.png is a binary file (37953 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/doc/images/table_editor/table.png is a binary file (24967 bytes)
/tmp/0pdd20181225-4-1mm7vgw/fidals/stroyprombeton/doc/images/table_editor/search_field.png is a binary file (6952 bytes)
Reading README.md...
Reading .0pdd.yml...
Reading assets/.gitignore...
Reading stroyprombeton/management/commands/seo_texts.py...
Reading stroyprombeton/management/commands/fake_db.py...
Reading stroyprombeton/management/commands/remove_product_duplicates.py...
Reading stroyprombeton/management/commands/untranslated.py...
Reading stroyprombeton/management/commands/price.py...
Reading stroyprombeton/management/commands/category_images.py...
Reading stroyprombeton/management/commands/translate.py...
Reading stroyprombeton/management/commands/test_db.py...
Puzzle 296-027bf696 60/DEV at stroyprombeton/management/commands/test_db.py
Reading stroyprombeton/__init__.py...
Reading stroyprombeton/urls.py...
Puzzle 187-598bff5b 60/DEV at stroyprombeton/urls.py
Reading stroyprombeton/views/__init__.py...
Reading stroyprombeton/views/admin.py...
Reading stroyprombeton/views/pages.py...
Reading stroyprombeton/views/search.py...
Reading stroyprombeton/views/ecommerce.py...
Reading stroyprombeton/views/helpers.py...
Reading stroyprombeton/views/catalog.py...
Reading stroyprombeton/settings/local.py.dist...
Reading stroyprombeton/settings/prod.py...
Reading stroyprombeton/settings/base.py...
Reading stroyprombeton/settings/dev.py...
Reading stroyprombeton/context.py...
Reading stroyprombeton/context_processors.py...
Reading stroyprombeton/templatetags/stb_extras.py...
Reading stroyprombeton/migrations/0007_remove_category_link_to_metal.py...
Reading stroyprombeton/migrations/0003_auto_20161229_0848.py...
Reading stroyprombeton/migrations/0004_zero_code_to_none.py...
Reading stroyprombeton/migrations/0011_add_tags.py...
Reading stroyprombeton/migrations/0002_delete_region.py...
Reading stroyprombeton/migrations/0012_product_tags.py...
Reading stroyprombeton/migrations/0006_auto_20170317_1457.py...
Reading stroyprombeton/migrations/0001_initial.py...
Reading stroyprombeton/migrations/0005_alter_catalog_fields.py...
Reading stroyprombeton/migrations/0008_page_templates.py...
Reading stroyprombeton/migrations/0010_update_robots_slug.py...
Reading stroyprombeton/migrations/0013_buff_tag_name.py...
Reading stroyprombeton/migrations/0009_trigram_search.py...
Reading stroyprombeton/locale/ru/LC_MESSAGES/django.po...
Reading stroyprombeton/tasks.py...
Reading stroyprombeton/fixtures/admin.json...
Reading stroyprombeton/fixtures/dump.json...
Reading stroyprombeton/admin.py...
Reading stroyprombeton/mailer.py...
Reading stroyprombeton/celery.py...
Reading stroyprombeton/apps.py...
Reading stroyprombeton/forms.py...
Reading stroyprombeton/tests/tests_admin.py...
Reading stroyprombeton/tests/tests_selenium_mobile.py...
Reading stroyprombeton/tests/tests_views.py...
Puzzle 340-6466f49e 60/DEV at stroyprombeton/tests/tests_views.py
Puzzle 142-0a53b98c 30/DEV at stroyprombeton/tests/tests_views.py
Puzzle 315-9094883c 60/DEV at stroyprombeton/tests/tests_views.py
Puzzle 374-3d4dc0fa 30/DEV at stroyprombeton/tests/tests_views.py
Reading stroyprombeton/tests/tests_forms.py...
Reading stroyprombeton/tests/tests_commands.py...
Reading stroyprombeton/tests/helpers.py...
Reading stroyprombeton/tests/tests_selenium.py...
ERROR: stroyprombeton/tests/tests_selenium.py; puzzle at line #331; TODO found, but puzzle can't be parsed, most probably because TODO is not followed by a puzzle marker, as this page explains: https://github.com/yegor256/pdd#how-to-format
If you can't understand the cause of this issue or you don't know how to fix it, please submit a GitHub issue, we will try to help you: https://github.com/yegor256/pdd/issues. This tool is still in its beta version and we will appreciate your feedback. Here is where you can find more documentation: https://github.com/yegor256/pdd/blob/master/README.md.
Exit code is 1

/app/objects/git_repo.rb:66:in `rescue in block in xml'
/app/objects/git_repo.rb:63:in `block in xml'
/app/vendor/ruby-2.5.1/lib/ruby/2.5.0/tempfile.rb:295:in `open'
/app/objects/git_repo.rb:62:in `xml'
/app/objects/puzzles.rb:36:in `deploy'
/app/objects/job.rb:38:in `proceed'
/app/objects/job_starred.rb:33:in `proceed'
/app/objects/job_recorded.rb:32:in `proceed'
/app/objects/job_emailed.rb:35:in `proceed'
/app/objects/job_commiterrors.rb:36:in `proceed'
/app/objects/job_detached.rb:48:in `exclusive'
/app/objects/job_detached.rb:36:in `block in proceed'
/app/objects/job_detached.rb:36:in `fork'
/app/objects/job_detached.rb:36:in `proceed'
/app/0pdd.rb:354:in `block in <top (required)>'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1635:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1635:in `block in compile!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:992:in `block (3 levels) in route!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1011:in `route_eval'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:992:in `block (2 levels) in route!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1040:in `block in process_route'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1038:in `catch'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1038:in `process_route'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:990:in `block in route!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:989:in `each'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:989:in `route!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1097:in `block in dispatch!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `block in invoke'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `catch'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `invoke'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1094:in `dispatch!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:924:in `block in call!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `block in invoke'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `catch'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1076:in `invoke'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:924:in `call!'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:913:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/xss_header.rb:18:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/path_traversal.rb:16:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/json_csrf.rb:26:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/base.rb:50:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/base.rb:50:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-protection-2.0.4/lib/rack/protection/frame_options.rb:31:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/logger.rb:15:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/common_logger.rb:33:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:231:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:224:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/head.rb:12:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/method_override.rb:22:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:194:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1957:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1502:in `block in call'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1729:in `synchronize'
/app/vendor/bundle/ruby/2.5.0/gems/sinatra-2.0.4/lib/sinatra/base.rb:1502:in `call'
/app/vendor/bundle/ruby/2.5.0/gems/rack-2.0.6/lib/rack/handler/webrick.rb:86:in `service'
/app/vendor/ruby-2.5.1/lib/ruby/2.5.0/webrick/httpserver.rb:140:in `service'
/app/vendor/ruby-2.5.1/lib/ruby/2.5.0/webrick/httpserver.rb:96:in `run'
/app/vendor/ruby-2.5.1/lib/ruby/2.5.0/webrick/server.rb:307:in `block in start_thread'

Please sign in to comment.