diff --git a/.github/prtester.py b/.github/prtester.py
index df6cc1ff..a2a7ab84 100644
--- a/.github/prtester.py
+++ b/.github/prtester.py
@@ -1,113 +1,159 @@
+import argparse
import requests
-import itertools
from bs4 import BeautifulSoup
from datetime import datetime
+from typing import Iterable
import os.path
# This script is specifically written to be used in automation for https://github.com/RSS-Bridge/rss-bridge
#
# This will scrape the whitelisted bridges in the current state (port 3000) and the PR state (port 3001) of
# RSS-Bridge, generate a feed for each of the bridges and save the output as html files.
-# It also replaces the default static CSS link with a hardcoded link to @em92's public instance, so viewing
+# It also add a tag with the url of em's public instance, so viewing
# the HTML file locally will actually work as designed.
-def testBridges(bridges,status):
- for bridge in bridges:
- if bridge.get('data-ref'): # Some div entries are empty, this ignores those
- bridgeid = bridge.get('id')
- bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
- print(bridgeid + "\n")
- bridgestring = '/?action=display&bridge=' + bridgeid + '&format=Html'
- forms = bridge.find_all("form")
- formid = 1
- for form in forms:
- # a bridge can have multiple contexts, named 'forms' in html
- # this code will produce a fully working formstring that should create a working feed when called
- # this will create an example feed for every single context, to test them all
- formstring = ''
- errormessages = []
- parameters = form.find_all("input")
- lists = form.find_all("select")
- # this for/if mess cycles through all available input parameters, checks if it required, then pulls
- # the default or examplevalue and then combines it all together into the formstring
- # if an example or default value is missing for a required attribute, it will throw an error
- # any non-required fields are not tested!!!
- for parameter in parameters:
- if parameter.get('type') == 'hidden' and parameter.get('name') == 'context':
- cleanvalue = parameter.get('value').replace(" ","+")
- formstring = formstring + '&' + parameter.get('name') + '=' + cleanvalue
- if parameter.get('type') == 'number' or parameter.get('type') == 'text':
- if parameter.has_attr('required'):
- if parameter.get('placeholder') == '':
- if parameter.get('value') == '':
- errormessages.append(parameter.get('name'))
- else:
- formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('value')
+class Instance:
+ name = ''
+ url = ''
+
+def main(instances: Iterable[Instance], with_upload: bool, comment_title: str):
+ start_date = datetime.now()
+ table_rows = []
+ for instance in instances:
+ page = requests.get(instance.url) # Use python requests to grab the rss-bridge main page
+ soup = BeautifulSoup(page.content, "html.parser") # use bs4 to turn the page into soup
+ bridge_cards = soup.select('.bridge-card') # get a soup-formatted list of all bridges on the rss-bridge page
+ table_rows += testBridges(instance, bridge_cards, with_upload) # run the main scraping code with the list of bridges and the info if this is for the current version or the pr version
+ with open(file=os.getcwd() + '/comment.txt', mode='w+', encoding='utf-8') as file:
+ table_rows_value = '\n'.join(sorted(table_rows))
+ file.write(f'''
+## {comment_title}
+| Bridge | Context | Status |
+| - | - | - |
+{table_rows_value}
+
+*last change: {start_date.strftime("%A %Y-%m-%d %H:%M:%S")}*
+ '''.strip())
+
+def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool) -> Iterable:
+ instance_suffix = ''
+ if instance.name:
+ instance_suffix = f' ({instance.name})'
+ table_rows = []
+ for bridge_card in bridge_cards:
+ bridgeid = bridge_card.get('id')
+ bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
+ print(f'{bridgeid}{instance_suffix}\n')
+ bridgestring = '/?action=display&bridge=' + bridgeid + '&format=Html'
+ bridge_name = bridgeid.replace('Bridge', '')
+ context_forms = bridge_card.find_all("form")
+ form_number = 1
+ for context_form in context_forms:
+ # a bridge can have multiple contexts, named 'forms' in html
+ # this code will produce a fully working formstring that should create a working feed when called
+ # this will create an example feed for every single context, to test them all
+ formstring = ''
+ error_messages = []
+ context_name = '*untitled*'
+ context_name_element = context_form.find_previous_sibling('h5')
+ if context_name_element and context_name_element.text.strip() != '':
+ context_name = context_name_element.text
+ parameters = context_form.find_all("input")
+ lists = context_form.find_all("select")
+ # this for/if mess cycles through all available input parameters, checks if it required, then pulls
+ # the default or examplevalue and then combines it all together into the formstring
+ # if an example or default value is missing for a required attribute, it will throw an error
+ # any non-required fields are not tested!!!
+ for parameter in parameters:
+ if parameter.get('type') == 'hidden' and parameter.get('name') == 'context':
+ cleanvalue = parameter.get('value').replace(" ","+")
+ formstring = formstring + '&' + parameter.get('name') + '=' + cleanvalue
+ if parameter.get('type') == 'number' or parameter.get('type') == 'text':
+ if parameter.has_attr('required'):
+ if parameter.get('placeholder') == '':
+ if parameter.get('value') == '':
+ name_value = parameter.get('name')
+ error_messages.append(f'Missing example or default value for parameter "{name_value}"')
else:
- formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('placeholder')
- # same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the formstring
- if parameter.get('type') == 'checkbox':
- if parameter.has_attr('checked'):
- formstring = formstring + '&' + parameter.get('name') + '=on'
- for listing in lists:
- selectionvalue = ''
- listname = listing.get('name')
- cleanlist = []
- for option in listing.contents:
- if 'optgroup' in option.name:
- cleanlist.extend(option)
+ formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('value')
else:
- cleanlist.append(option)
- firstselectionentry = 1
- for selectionentry in cleanlist:
- if firstselectionentry:
+ formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('placeholder')
+ # same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the formstring
+ if parameter.get('type') == 'checkbox':
+ if parameter.has_attr('checked'):
+ formstring = formstring + '&' + parameter.get('name') + '=on'
+ for listing in lists:
+ selectionvalue = ''
+ listname = listing.get('name')
+ cleanlist = []
+ for option in listing.contents:
+ if 'optgroup' in option.name:
+ cleanlist.extend(option)
+ else:
+ cleanlist.append(option)
+ firstselectionentry = 1
+ for selectionentry in cleanlist:
+ if firstselectionentry:
+ selectionvalue = selectionentry.get('value')
+ firstselectionentry = 0
+ else:
+ if 'selected' in selectionentry.attrs:
selectionvalue = selectionentry.get('value')
- firstselectionentry = 0
- else:
- if 'selected' in selectionentry.attrs:
- selectionvalue = selectionentry.get('value')
- break
- formstring = formstring + '&' + listname + '=' + selectionvalue
- if not errormessages:
- # if all example/default values are present, form the full request string, run the request, replace the static css
- # file with the url of em's public instance and then upload it to termpad.com, a pastebin-like-site.
- r = requests.get(URL + bridgestring + formstring)
- pagetext = r.text.replace('static/style.css','https://rss-bridge.org/bridge01/static/style.css')
- pagetext = pagetext.encode("utf_8")
- termpad = requests.post(url="https://termpad.com/", data=pagetext)
- termpadurl = termpad.text
- termpadurl = termpadurl.replace('termpad.com/','termpad.com/raw/')
- termpadurl = termpadurl.replace('\n','')
- with open(os.getcwd() + '/comment.txt', 'a+') as file:
- file.write("\n")
- file.write("| [`" + bridgeid + '-' + status + '-context' + str(formid) + "`](" + termpadurl + ") | " + date_time + " |")
+ break
+ formstring = formstring + '&' + listname + '=' + selectionvalue
+ termpad_url = 'about:blank'
+ if error_messages:
+ status = '
'.join(map(lambda m: f'❌ `{m}`', error_messages))
+ else:
+ # if all example/default values are present, form the full request string, run the request, add a tag with
+ # the url of em's public instance to the response text (so that relative paths work, e.g. to the static css file) and
+ # then upload it to termpad.com, a pastebin-like-site.
+ response = requests.get(instance.url + bridgestring + formstring)
+ page_text = response.text.replace('
','')
+ page_text = page_text.encode("utf_8")
+ soup = BeautifulSoup(page_text, "html.parser")
+ status_messages = list(map(lambda e: f'⚠️ `{e.text.strip().splitlines()[0]}`', soup.find_all('pre')))
+ if response.status_code != 200:
+ status_messages = [f'❌ `HTTP status {response.status_code} {response.reason}`'] + status_messages
else:
- # if there are errors (which means that a required value has no example or default value), log out which error appeared
- termpad = requests.post(url="https://termpad.com/", data=str(errormessages))
- termpadurl = termpad.text
- termpadurl = termpadurl.replace('termpad.com/','termpad.com/raw/')
- termpadurl = termpadurl.replace('\n','')
- with open(os.getcwd() + '/comment.txt', 'a+') as file:
- file.write("\n")
- file.write("| [`" + bridgeid + '-' + status + '-context' + str(formid) + "`](" + termpadurl + ") | " + date_time + " |")
- formid += 1
+ feed_items = soup.select('.feeditem')
+ feed_items_length = len(feed_items)
+ if feed_items_length <= 0:
+ status_messages += [f'⚠️ `The feed has no items`']
+ elif feed_items_length == 1 and len(soup.select('.error')) > 0:
+ status_messages = [f'❌ `{feed_items[0].text.strip().splitlines()[0]}`'] + status_messages
+ status = '
'.join(status_messages)
+ if status.strip() == '':
+ status = '✔️'
+ if with_upload:
+ termpad = requests.post(url="https://termpad.com/", data=page_text)
+ termpad_url = termpad.text.strip()
+ termpad_url = termpad_url.replace('termpad.com/','termpad.com/raw/')
+ table_rows.append(f'| {bridge_name} | [{form_number} {context_name}{instance_suffix}]({termpad_url}) | {status} |')
+ form_number += 1
+ return table_rows
-gitstatus = ["current", "pr"]
-now = datetime.now()
-date_time = now.strftime("%Y-%m-%d, %H:%M:%S")
-
-with open(os.getcwd() + '/comment.txt', 'w+') as file:
- file.write(''' ## Pull request artifacts
-| file | last change |
-| ---- | ------ |''')
-
-for status in gitstatus: # run this twice, once for the current version, once for the PR version
- if status == "current":
- port = "3000" # both ports are defined in the corresponding workflow .yml file
- elif status == "pr":
- port = "3001"
- URL = "http://localhost:" + port
- page = requests.get(URL) # Use python requests to grab the rss-bridge main page
- soup = BeautifulSoup(page.content, "html.parser") # use bs4 to turn the page into soup
- bridges = soup.find_all("section") # get a soup-formatted list of all bridges on the rss-bridge page
- testBridges(bridges,status) # run the main scraping code with the list of bridges and the info if this is for the current version or the pr version
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-i', '--instances', nargs='+')
+ parser.add_argument('-nu', '--no-upload', action='store_true')
+ parser.add_argument('-t', '--comment-title', default='Pull request artifacts')
+ args = parser.parse_args()
+ instances = []
+ if args.instances:
+ for instance_arg in args.instances:
+ instance_arg_parts = instance_arg.split('::')
+ instance = Instance()
+ instance.name = instance_arg_parts[1] if len(instance_arg_parts) >= 2 else ''
+ instance.url = instance_arg_parts[0]
+ instances.append(instance)
+ else:
+ instance = Instance()
+ instance.name = 'current'
+ instance.url = 'http://localhost:3000'
+ instances.append(instance)
+ instance = Instance()
+ instance.name = 'pr'
+ instance.url = 'http://localhost:3001'
+ instances.append(instance)
+ main(instances=instances, with_upload=not args.no_upload, comment_title=args.comment_title);
\ No newline at end of file