test_book_webapp.py - Unit tests

Invocation:

Imports

These are listed in the order prescribed by PEP 8.

Standard library

import json

Third-party imports

import pytest
import stripe
import flask_user

Local imports

from app import app
from models import (
    db, Book, Page, Question, Answer, Feedback, Annotations, Annotation, Class_,
    Instructor, Author, User, Payment, create_db, UserType, sphinxImport,
    make_user, GraderType
)

Need this to make the app work, even if we don’t directly use these imports.

import book_webapp

Tests

Data

Creates some fake data which the tests use. Create test data.

def create_test_data():
    visitor = make_user('visitor', UserType.user)
    student = make_user('student', UserType.user)
    instructor = make_user('instructor', UserType.user)
    author = make_user('author', UserType.author)
    admin = make_user('admin', UserType.admin)
    if not db.session.query(Book).filter(Book.url == 'book').all():
        class_ = Class_(
            name='test_class',
            instructor=[Instructor(
               user_id=instructor.id)
            ]
        )
        book = Book(
            url='book',
            title='A test book',
            price=1000,
            page=[
                Page(
                    url='unsigned_8-_and_16-bit_ops/introduction.s.html',
                    index=1,
                    question=[
                        Question(
                            label='define_label',
                            index=1,
                            pointsPossible=2,
                            grader=GraderType.exact,
                            answer=[
                                Answer(
                                    user_id=student.id,
                                    string='foo:',
                                    points=2,
                                ),
                            ],
                            feedback=[
                                Feedback(
                                    answer='foo:',
                                    feedback='correct',
                                    points=2
                                ), Feedback(
                                    answer='',
                                    feedback='Wrong.',
                                    points=0,
                                ),
                            ],
                        ), Question(
                            label='comment',
                            index=2,
                            pointsPossible=1,
                            grader=GraderType.exact,
                            answer=[
                                Answer(
                                    user_id=student.id,
                                    string='10000',
                                    points=1,
                                ),
                            ],
                        ), Question(
                            label='space_directive',
                            index=3,
                            pointsPossible=3,
                            grader=GraderType.ws_ignore,
                            feedback=[
                                Feedback(
                                    answer='.space u16_a',
                                    feedback='y',
                                    points=3,
                                ), Feedback(
                                    answer='.space',
                                    feedback='first half',
                                    points=2,
                                ), Feedback(
                                    answer='u16_a',
                                    feedback='second half',
                                    points=1,
                                ), Feedback(
                                    answer='',
                                    feedback='n',
                                    points=0,
                                ),
                            ],
                        ), Question(
                            label='mov_instruction',
                            index=4,
                            pointsPossible=2,
                            grader=GraderType.regexp_nocase,
                            feedback=[
                                Feedback(
                                    answer='\s*mov\s+u16_a\s*,\s*W0\s*',
                                    feedback='y',
                                    points=2,
                                ), Feedback(
                                    answer='\s*mov\s+u16_a\s*,\s*WREG\s*',
                                    feedback='WREG',
                                    points=1,
                                ), Feedback(
                                    answer='',
                                    feedback='n',
                                    points=0,
                                ),
                            ],
                        ), Question(
                            label='hex_value',
                            index=5,
                            pointsPossible=5,
                            grader=GraderType.number,
                            feedback=[
                                Feedback(
                                    answer='0x12',
                                    feedback='hex',
                                    points=1,
                                ), Feedback(
                                    answer='0b101',
                                    feedback='bin',
                                    points=2,
                                ), Feedback(
                                    answer='0o77',
                                    feedback='oct',
                                    points=3,
                                ), Feedback(
                                    answer='1.23',
                                    points=4,
                                    feedback='float',
                                ), Feedback(
                                    answer='2.2-3.3',
                                    feedback='range',
                                    points=5,
                                ), Feedback(
                                    answer='',
                                    feedback='n',
                                    points=0,
                                ),
                            ],
                        ),
                    ],
                ),
            ],
            class_=[
                class_,
                Class_(
                   name='Another test class',
                   price=500,
                ),
            ],
            author=[
                Author(user_id=author.id),
            ],
        )
        db.session.add(book)
    else:
        book = db.session.query(Book).filter(Book.url == 'book').one()
        class_ = db.session.query(Class_).filter(Class_.id == book.id).filter(Class_.name == 'test_class').one()

    if not student.payment.all():
        student.payment = [Payment(
            class_id=class_.id,
            charge_id='xxx'
        )]
    db.session.commit()

Fixtures

Set up the database for the test session. Do this once per file. (I don’t see any performance difference in doing this, as opposed to performing it for every test.)

@pytest.fixture(scope='module')
def test_db():
    app.config.update(

In-memory sqlite DB

        SQLALCHEMY_DATABASE_URI='sqlite:///:memory:',

Propagate exceptions (don’t show 500 error page).

        TESTING=True,

Disable CSRF token in Flask-Wtf.

        WTF_CSRF_ENABLED=False,

Enable @register_required while app.testing=True.

        LOGIN_DISABLED=False,

Suppress the sending of emails.

        MAIL_SUPPRESS_SEND=True,

Enable url_for() without request context.

        SERVER_NAME='localhost',

Use test keys, not live keys.

        STRIPE_PRIVATE_KEY='sk_test_IrRibQTodX0xq299PzRJdnW8',
        STRIPE_PUBLIC_KEY='pk_test_JvFCdXUEdhIuKsCFCbcL0jvH',
    )

    test_client = app.test_client()
    with app.app_context():
        create_db()

    yield test_client
 

Define per-function setup and teardown which sets places test data in an already existing database.

@pytest.fixture()
def test_client(test_db, monkeypatch):

When testing, password hashing dominates test time; disable it to speed the tests. See https://pythonhosted.org/Flask-User/customization.html#password-hashing.

    def hash_password(self, password):
        return password

    def verify_password(self, password, user):
        return self.hash_password(password) == self.get_password(user)

    monkeypatch.setattr(flask_user.UserManager, 'hash_password', hash_password)
    monkeypatch.setattr(flask_user.UserManager, 'verify_password', verify_password)
 

Setup

    with app.app_context():
        create_test_data()
    yield test_db
 

Teardown. Adapted from http://stackoverflow.com/a/5003705. A simple db.drop_all() works, but doubles test time. This should remove all data, but keep the schema.

    for table in reversed(db.metadata.sorted_tables):
        db.session.execute(table.delete())
    db.session.commit()
 

Utilities

get_check: Get a web page, checking its return code and optionally its contents.

def get_check(

test_client: the test client to request pages from.

  test_client,

url: The bare URL to reqeust (so that / refers to the root of the web site).

  url,

The expected status code returned by the web server. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for a list of all codes.

  expected_status,

expected_response_phrase: A phrase which must be in the text returned. The type must be bytes; the default argument of b'' skips this check.

  expected_response_phrase=b'',

kwargs: Any additional keyword arguments to pass to test_client.get, such as follow_redirects=True.

  **kwargs):
 

The call to test_client.get returns a response object.

    rv = test_client.get(url, **kwargs)
    try:

Check the status code and the data.

        assert rv.status_code == expected_status
        assert expected_response_phrase in rv.data
    except AssertionError:

On a test failure, save the resulting web page for debug purposes.

        with open('tmp.html', 'wb') as f:
            f.write(rv.data)
        raise
    return rv
 

get_valid: Get a web page, verifying the status code was 200 (OK). This function returns the value produced by get_check.

def get_valid(
  test_client,

See url.

  url,

Optionally provide the expected_response_phrase.

  *args,

See kwargs.

  **kwargs):

    return get_check(test_client, url, 200, *args, **kwargs)
 

After get_valid, check that the returned data is the expected, JSON-formatted dict. This function returns the value produced by get_valid.

def get_valid_json(
  test_client,

See url.

  url,

The expected response dictionary produce by interpreting the response data as UTF-8 encoded JSON.

  expected_response_dict,

See kwargs.

  **kwargs):

    rv = get_valid(test_client, url, **kwargs)
    assert json.loads(str(rv.data, encoding='utf-8')) == expected_response_dict
    return rv
 

Get a web page, verifying the status code was 404 (not found). This function returns the value produced by get_check.

def get_invalid(
  test_client,

See url.

  url,

See kwargs.

  **kwargs):

    return get_check(test_client, url, 404, b'seem to find the page you',
        **kwargs)
 

Verify that the given url refers to the index page. This function returns the value produced by get_valid.

def index_check(
  test_client,

See url.

  url,

See kwargs.

  **kwargs):

    return get_valid(test_client, url, b'This textbook introduces assembly',
        **kwargs)
 

Verify that a login with the given username succeeds.

def login(test_client, username, password='default'):
    rv = test_client.post(app.config['USER_LOGIN_URL'], data=dict(
            username=username,
            password=password,
        ),
        follow_redirects=True)
    assert b'This textbook introduces assembly' in rv.data
    assert b'You have signed in successfully.' in rv.data
    assert rv.status_code == 200
    return rv
 

Verify that a logout succeeds.

def logout(test_client):
    return get_valid(test_client, app.config['USER_LOGOUT_URL'],
        b'You have signed out successfully.', follow_redirects=True)
 

Define a context manger which sandwiches its body with a login/logout.

class LoginContext:
    def __init__(self, test_client, username, *args):
        self.test_client = test_client
        self.username = username
        self.args = args

    def __enter__(self):
        return login(self.test_client, self.username, *self.args)

    def __exit__(self, exc_type, exc_value, traceback):
        logout(self.test_client)

def is_login(test_client, url):
    logout(test_client)
    return get_valid(test_client, url,
        b'Please log in to access this page.', follow_redirects=True)

def is_payment(test_client, url):
    with LoginContext(test_client, 'visitor'):
        return get_valid(test_client, url, b'Select a class', follow_redirects=True)
 

Convert the database to a string.

def dbToStr(test_client):
    s = []
    for table in reversed(db.metadata.sorted_tables):
        s += ['', str(table)]
        s += [str(x) for x in db.session.query(table).all()]
    return '\n'.join(s[1:])
 

Conver the database to a prettyprinted string.

def dbToOutline(test_client):
    s = []
    for book in db.session.query(Book):
        s += [
            '- Book id = {}'.format(book.id),
            '   - url = {}'.format(book.url),
            '   - title = {}'.format(book.title),
            '   - price = {}'.format(book.price),
        ]
        for page in book.page:
            s += [
                '   - Page id = {}'.format(page.id),
                '       - url = {}'.format(page.url),
                '       - index = {}'.format(page.index),
            ]
            for question in page.question:
                s += [
                    '       - Question id = {}'.format(question.id),
                    '           - label = {}'.format(question.label),
                    '           - index = {}'.format(question.index),
                    '           - pointsPosssible = {}'.format(question.pointsPossible),
                ]
                for answer in question.answer:
                    s += [
                        '           - Answer id = {}'.format(answer.id),
                        '               - User id = {}'.format(answer.user_id),
                        '               - string = {}'.format(answer.string),
                        '               - points = {}'.format(answer.points),
                    ]
                for feedback in question.feedback:
                    s += [
                        '           - Feedback id = {}'.format(feedback.id),
                        '               - answer = {}'.format(feedback.answer),
                        '               - feedback = {}'.format(feedback.feedback),
                        '               - points = {}'.format(feedback.points),
                    ]
            for annotations in page.annotations:
                s += [
                    '       - Annotations id = {}'.format(annotations.id),
                    '           - User id = {}'.format(annotations.user_id),
                ]
                for annotation in annotations.annotation:
                    s += [
                        '           - Annotation id = {}'.format(annotation.id),
                        '               - target = {}'.format(annotation.target),
                        '               - data = {}'.format(annotation.data),
                    ]
        for class_ in book.class_:
            s += [
                '   - Class_ id = {}'.format(class_.id),
                '       - name = {}'.format(class_.name),
                '       - price = {}'.format(class_.price),
            ]
            for instructor in class_.instructor:
                s += [
                    '       - Instructor id = {}'.format(instructor.id),
                    '           - User id = {}'.format(instructor.user_id),
                ]
        for author in book.author:
            s += [
                '   - Author id = {}'.format(author.id),
                '       - User id = {}'.format(author.user_id),
            ]
    for user in db.session.query(User):
        s += [
            '- User id = {}'.format(user.id),
            '   - username = {}'.format(user.username),
            '   - password = {}'.format(user.password),
            '   - reset_password_token = {}'.format(user.reset_password_token),
            '   - email = {}'.format(user.email),
            '   - confirmed_at = {}'.format(user.confirmed_at),
            '   - active = {}'.format(user.active),
            '   - first_name = {}'.format(user.first_name),
            '   - last_name = {}'.format(user.last_name),
            '   - type_ = {}'.format(user.type_),
        ]
        for payment in user.payment:
            s += [
                '   - Payment id = {}'.format(payment.id),
                '       - Class id = {}'.format(payment.class_id),
                '       - charge_id = {}'.format(payment.charge_id),
            ]
    return '\n'.join(s)

Tests

Check the root path view and index.html.

def test_1(test_client):
    index_check(test_client, '/')
    index_check(test_client, '/index.html')
 

Check the 404 page at several levels of the hierarchy.

def test_3(test_client):
    is_login(test_client, '/xxx.html')
    with LoginContext(test_client, 'student'):
        get_invalid(test_client, '/xxx.html')

def test_4(test_client):
    is_login(test_client, '/book/xxx.html')
    with LoginContext(test_client, 'student'):
        get_invalid(test_client, '/book/xxx.html')
        get_invalid(test_client, '/invalid_book/xxx.html')
 

Check acesss to non-book files.

def test_5(test_client):
    get_valid(test_client, '/conf.py.html',
        b'Configuration file for this Sphinx CodeChat project')

def test_6(test_client):
    get_valid(test_client, '/build/Flask/book_webapp.py.html',
        b'A web application hosting the book website')
 

Check a basic login/logout.

siaa_users = ('student', 'instructor', 'author', 'admin')
def test_7(test_client):
    for user in siaa_users:
        with LoginContext(test_client, 'student'):
            pass
 

Check access to book files.

references_url = '/book/references.html'
def test_8(test_client):
    is_login(test_client, references_url)
    is_payment(test_client, references_url)

    for user in siaa_users:
        with LoginContext(test_client, user):
            get_valid(test_client, references_url, b'Family reference manual')
 

Check getting stored answers to questions

questions_url = '/book/unsigned_8-_and_16-bit_ops/introduction.s.html/questions'
def test_9(test_client):
    is_login(test_client, '/book/xxx/questions')
    with LoginContext(test_client, 'student'):
        get_invalid(test_client, '/book/xxx/questions')

    is_login(test_client, questions_url)
    is_payment(test_client, questions_url)

    with LoginContext(test_client, 'student'):
        get_valid_json(test_client, questions_url,
                       {'define_label': ['foo:', 2, 2, 'correct'],
                        'comment': ['10000', 0, 1, 'Incorrect.']})

    for user in ['instructor', 'author', 'admin']:
        with LoginContext(test_client, user):
            get_valid_json(test_client, questions_url, {})
 

Check saving stored answers.

def test_10(test_client):
    is_login(test_client, '/book/xxx/questions?a=b')
    with LoginContext(test_client, 'student'):
        get_invalid(test_client, '/book/xxx/questions?a=b')

    is_login(test_client, questions_url + '?define_label=foo:')
    is_payment(test_client, questions_url + '?define_label=foo:')

    for user in siaa_users:
        with LoginContext(test_client, user):

Test a really long answer.

            get_valid_json(test_client, questions_url + '?define_label=' + ('x'*1000),
                           {'define_label': [0, 2, 'Wrong.']})
 

Test a correct answer.

            get_valid_json(test_client, questions_url + '?define_label=foo:',
                           {'define_label': [2, 2, 'correct']})
 

Test a wrong answer

            get_valid_json(test_client, questions_url + '?define_label=foo',
                           {'define_label': [0, 2, 'Wrong.']})
 

Test an answer to a non-existent question.

            get_invalid(test_client, questions_url + '?no_label=foo')
 

Test adding a new answer, and default feedback.

            get_valid_json(test_client, questions_url + '?comment=bar',
                           {'comment': [0, 1, 'Incorrect.']})
 

Test submitting multiple answers.

            get_valid_json(test_client, questions_url + '?define_label=food&comment=bard',
                           {'define_label': [0, 2, 'Wrong.'],
                           'comment': [0, 1, 'Incorrect.']})
 

Make sure these updated answers can be read back.

            get_valid_json(test_client, questions_url,
                           {'define_label': ['food', 0, 2, 'Wrong.'],
                            'comment': ['bard', 0, 1, 'Incorrect.']})
 

Check that the ws_ignore grader works correctly.

            get_valid_json(test_client, questions_url + '?space_directive=%20.space%20%20u16_a%20',
                           {'space_directive': [3, 3, 'y']})
            get_valid_json(test_client, questions_url + '?space_directive=%20.space%20%20%20',
                           {'space_directive': [2, 3, 'first half']})
            get_valid_json(test_client, questions_url + '?space_directive=%20%20%20u16_a%20',
                           {'space_directive': [1, 3, 'second half']})
            get_valid_json(test_client, questions_url + '?space_directive=%20.spaced%20%20u16_a%20',
                           {'space_directive': [0, 3, 'n']})
 

Check the regex graders.

            get_valid_json(test_client, questions_url + '?mov_instruction=%20mov%20%20u16_a,W0%20',
                           {'mov_instruction': [2, 2, 'y']})
            get_valid_json(test_client, questions_url + '?mov_instruction=Mov%20u16_a,W0',
                           {'mov_instruction': [2, 2, 'y']})
            get_valid_json(test_client, questions_url + '?mov_instruction=mov%20u16_a,%20Wreg',
                           {'mov_instruction': [1, 2, 'WREG']})
            get_valid_json(test_client, questions_url + '?mov_instruction=movx%20u16_a,%20WREG',
                           {'mov_instruction': [0, 2, 'n']})
 

Check the number grader.

            get_valid_json(test_client, questions_url + '?hex_value=0x12',
                           {'hex_value': [1, 5, 'hex']})
            get_valid_json(test_client, questions_url + '?hex_value=0b101',
                           {'hex_value': [2, 5, 'bin']})
            get_valid_json(test_client, questions_url + '?hex_value=0o77',
                           {'hex_value': [3, 5, 'oct']})
            get_valid_json(test_client, questions_url + '?hex_value=1.23',
                           {'hex_value': [4, 5, 'float']})
            get_valid_json(test_client, questions_url + '?hex_value=3',
                           {'hex_value': [5, 5, 'range']})
            get_valid_json(test_client, questions_url + '?hex_value=z',
                           {'hex_value': [0, 5, 'n']})
 

Check that the gradebook works.

def test_11(test_client):
    with LoginContext(test_client, 'student'):

The student should have 3/13 correct (2 points for question define_label, 1 point for question comment.

        response = get_valid(test_client, '/book/grades.html', b'3/13')

def create_stripe_token(number='4242424242424242', cvc='123'):
    return stripe.Token.create(
        card={
            'number': number,
            'exp_month': 12,
            'exp_year': 2027,
            'cvc': cvc
        },
    )

def stripe_post(test_client, classId, expected_response_phrase, stripe_token=None):
    if stripe_token is None:
        stripe_token = create_stripe_token()
    rv = test_client.post(
        app.config['PAYMENT_URL'] + '?classId={}'.format(classId),
        data=dict(
            stripeToken=stripe_token.id,
            stripeEmail='foo@bar.com',
        ),
        follow_redirects=True
    )
    if isinstance(expected_response_phrase, str):
        expected_response_phrase = [expected_response_phrase]
    for x in expected_response_phrase:
        assert x in rv.data
    return rv
 

Test payment. Note: this test is slow (about 17 sec), most of which is spent waiting on Stripe. See slow.

def test_slow_12(test_client):
    with LoginContext(test_client, 'visitor'):

Test an invalid class ID.

        get_valid(test_client, app.config['PAYMENT_URL'] + '?classId=x', b'Select a class', follow_redirects=True)
        get_valid(test_client, app.config['PAYMENT_URL'] + '?foo=bar', b'Select a class', follow_redirects=True)

Test a missing agree checkbox

        get_valid(test_client, app.config['PAYMENT_URL'] + '?classId=x&agree=off', b'Select a class', follow_redirects=True)
 

Test a valid class ID.

        classId1 = db.session.query(Class_.id).filter(Class_.name == 'test_class').scalar()
        rv = get_valid(test_client, app.config['PAYMENT_URL'] + '?classId={}'.format(classId1), b'Payment')
        assert b'A test book' in rv.data
        assert b'test_class' in rv.data
        assert b'$10.00' in rv.data
 

Test a class with a specified price.

        classId2 = db.session.query(Class_.id).filter(Class_.name == 'Another test class').scalar()
        rv = get_valid(test_client, app.config['PAYMENT_URL'] + '?classId={}'.format(classId2), b'Payment')
        assert b'A test book' in rv.data
        assert b'Another test class' in rv.data
        assert b'$5.00' in rv.data
 

Test a purchase of an invalid class code.

        stripe_post(test_client, 'x', b'Select a class')
 

Test a purchase with missing fields.

        stripeToken = create_stripe_token()
        rv = test_client.post(
            app.config['PAYMENT_URL'] + '?classId=1',
            data=dict(
                stripeToken=stripeToken.id,
            ),
            follow_redirects=True
        )
        assert rv.status_code == 400
 

Test a purchase with various card declined errors. See https://stripe.com/docs/testing#cards-responses.

        for card_num in (

Charge is declined with a card_declined code.

            '4000000000000002',

Charge is declined with a card_declined code and a fraudulent reason.

            '4100000000000019',

Charge is declined with an incorrect_cvc code.

            '4000000000000127',

Charge is declined with an expired_card code.

            '4000000000000069',

Charge is declined with a processing_error code.

            '4000000000000119',

Skip: Charge is declined with an incorrect_number code as the card number fails the Luhn check. This raises an error in create_stripe_token (test code).

            #'4242424242424241',
        ):
            stripe_post(test_client, classId1, b'Charge declined', create_stripe_token(card_num))
 

Test a valid purchase with no Payment beforehand.

        visitorUserId = make_user('visitor', UserType.user).id
        payment = db.session.query(Payment).filter(Payment.user_id == visitorUserId)
        assert payment.one_or_none() is None
        stripe_post(test_client, classId1, b'You have purchased A test book. Thank you for your payment of $10.00.')
        payment = db.session.query(Payment).filter(Payment.user_id == visitorUserId).scalar()
        assert payment is not None
        db.session.delete(payment)
        db.session.commit()
 

Test a valid purchase where the class specifies the price.

        payment = db.session.query(Payment).filter(Payment.user_id == visitorUserId)
        assert payment.one_or_none() is None
        stripe_post(test_client, classId2, b'You have purchased A test book. Thank you for your payment of $5.00.')
        payment = db.session.query(Payment).filter(Payment.user_id == visitorUserId).scalar()
        assert payment is not None
        db.session.delete(payment)
        db.session.commit()
 

Verify that we can’t pay twice (student) or can’t pay (instructor, author).

    for user in siaa_users:
        with LoginContext(test_client, user):

Try via a get request.

            rv = get_valid(
                test_client, app.config['PAYMENT_URL'] + '?classId={}'.format(classId1),
                b'You have already paid for this class.', follow_redirects=True
            )
 

Try via a post request.

            stripe_post(test_client, classId1, b'You have already paid for this class.')
 

Some notes of testing models.sphinxImport

To test, I need to create a pickle file, then a specific database (unless the one created by create_test_data is sufficient. Then, run the code. Then, walk the database to check if appropriate items were either added, updated, or deleted.

Heroku

Some notes on getting up and running with Heroku:

  1. Install the CLI client.
  2. From the book repo subdirectory, heroku git:remote -a interactive-ebooks. Replace interactive-ebooks with the app’s name you created using the Heroku GUI.
  3. Send the app to Heroku:
    1. Commit everything. Clean out all uncommitted files.
    2. Run deploy.bat - Deploy the e-book to Heroku.
  4. heroku addons:create heroku-postgresql
  5. heroku addons:create sendgrid:starter. See https://sendgrid.com/docs/Classroom/Basics/Email_Infrastructure/recommended_smtp_settings.html to get up and running.
  6. heroku ps:scale web=1
  7. heroku run bash
    1. cd build/Flask
    2. python book_webapp to load the database up. Manually create an admin user.

Platform-specific challenges:

  1. Microchip’s xc16 compilers for Linux.

    1. To install, sudo apt-get install lib32z1 before executing the .run file (which requires this library).

    2. To run locally, sudo apt-get install lib32stdc++6.

    3. To run on Heroku, we need to install packages. Therefore, use the apt buildpack.

      1. Add the buildpack, via heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-apt.
      2. Provide an Aptfile with a list of packages to install.

      But, this fails because:

      1. The default buildpack doesn’t set LD_LIBRARY_PATH for 32-bit libraries. So, we need to add export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/usr/lib32:/app/.apt/lib32 to the Heroku environment. To do this, execute heroku config:set LD_LIBRARY_PATH=/app/.apt/usr/lib32:/app/.apt/lib32

      2. Note that 32-bit programs depend on a 32-bit loader, which is typically installed as /lib/ld-linux.so.2, but (of course) isn’t here. The path to the loader is encoded directly into the ELF binary. To work around this:

        1. One option would be using patchelf –set-interpreter bin_name for each binary to run. (Note that wildcards aren’t supported – invoke this once on each binary.) This is what I’ve chosen to do. But, it must be repeated for every upgrade of the compiler.

        2. Another is to write a simple script to invoke the loader manually:

          #!/bin/sh
          /app/.apt/lib32/ld-linux.so.2 `dirname "$0"`/_bin/`basename "$0"` $*
          

          Then, move any binaries to execute to a _bin directory, putting the script above in its place. (I haven’t tested this approach.)

  2. Permissions for Linux files. Since I’m using a Windows PC, git permissions must be set on any binaries. After adding the files, use git update-index –chmod=+x path_to_file. Note that wildcards are not supported.