# -*- coding: utf-8 -*- """P5 测试:`foreach_account`(一轮把本机待发的号跑完)—— 临时库,不碰真实数据。""" import io import os import shutil import sys import tempfile sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') TMP = tempfile.mkdtemp(prefix='ac_p5_') os.environ['DATABASE_URL'] = 'sqlite:///' + TMP.replace(chr(92), '/') + '/ac.db' os.environ['DATA_VIDEO_DIR'] = os.path.join(TMP, 'videos') os.environ['DEPLOY_ENV'] = 'dev' os.environ['DB_ALLOW_SQLITE_FALLBACK'] = '1' # 仓库根 = scripts/tests/ 往上三级(不写死绝对路径,换机器也能跑) _ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, _ROOT) os.chdir(_ROOT) from flask import Flask # noqa: E402 from core import db_config, models # noqa: E402 from core import video_plan as vp # noqa: E402 from tasks.generic import task as gt # noqa: E402 DATE = '2026-09-12' fails = [] def check(label, got, want): ok = got == want if not ok: fails.append(label) print((' ✅ ' if ok else ' ❌ ') + label + ('' if ok else '\n 得到 %r\n 期望 %r' % (got, want))) app = Flask("t") uri = db_config.build_db_uri() app.config["SQLALCHEMY_DATABASE_URI"] = uri app.config["SQLALCHEMY_ENGINE_OPTIONS"] = db_config.engine_options(uri) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False models.db.init_app(app) with app.app_context(): models.db.create_all() vp.init_app(app) def seed(): """小王 2 条、小李 1 条、小张 1 条(未来)。""" with app.app_context(): for i, (nm, d, s) in enumerate([('小王', DATE, 1), ('小王', DATE, 2), ('小李', DATE, 1), ('小张', '2027-01-01', 1)]): models.db.session.add(models.VideoPlan( id='p%d' % i, nickname=nm, phone='', device_name='', douyin_id='', serial='', release_date=d, seq=s, seq_auto=False, title='t' + str(s), body='', video_file='x', video_name='x', status=vp.ST_READY, created_at=vp._now(), updated_at=vp._now())) models.db.session.commit() def status_of(): with app.app_context(): out = {} for r in models.VideoPlan.query.all(): out.setdefault(r.nickname, []).append(r.status) return {k: sorted(v) for k, v in out.items()} print('=== 1. 一轮把本机待发的号跑完(模拟"推+发成功")===') seed() w = gt.GenericStepsWorker("FAKE01", {}, ctx={"job_id": "t1", "device_name": "A01"}) order = [] def fake_exec_ok(d, steps, depth=0): """模拟一段正常的循环体:把当前 target_name 的下一条待发计划标成 done。""" name = w._vars.get('target_name') order.append(name) with app.app_context(): row = (models.VideoPlan.query.filter_by(nickname=name, status=vp.ST_READY) .order_by(models.VideoPlan.seq).first()) if row: row.status = vp.ST_DONE models.db.session.commit() w._exec_steps = fake_exec_ok w._exec_foreach_account(None, {"children": [{"type": "x"}], "date_source": "fixed", "fixed_date": DATE}) # 同日期同编号时按名字**码点**定序:李 U+674E < 王 U+738B → 小李先跑 check('两个号都跑过(小李 1 条 → 小王 2 条)', order, ['小李', '小王', '小王']) check('小王两条都发了', status_of()['小王'], ['done', 'done']) check('小李发了', status_of()['小李'], ['done']) check('未来的小张没动', status_of()['小张'], ['ready']) check('跑完把 target_name 清掉了', w._vars.get('target_name'), None) print('=== 2. 本机没登这个号 → 不卡死,直接换下一个 ===') with app.app_context(): models.VideoPlan.query.delete() models.db.session.commit() seed() w2 = gt.GenericStepsWorker("FAKE02", {}, ctx={"job_id": "t2"}) seen = [] def fake_exec_only_xiaowang(d, steps, depth=0): """只有小王的号在这台机器上:切到别人时什么都不做(模拟切号失败/守卫挡下)。""" name = w2._vars.get('target_name') seen.append(name) if name != '小王': return with app.app_context(): row = (models.VideoPlan.query.filter_by(nickname=name, status=vp.ST_READY) .order_by(models.VideoPlan.seq).first()) if row: row.status = vp.ST_DONE models.db.session.commit() w2._exec_steps = fake_exec_only_xiaowang w2._exec_foreach_account(None, {"children": [{"type": "x"}], "date_source": "fixed", "fixed_date": DATE}) check('小王发完(2 条)', status_of()['小王'], ['done', 'done']) check('小李只被试了 1 次就跳过(没空转)', seen.count('小李'), 1) check('试过的名字不回头', seen, ['小李', '小王', '小王']) check('小李还是待发', status_of()['小李'], ['ready']) print('=== 3. 下一轮会接着发小李(上一轮没发完的还在队列里)===') w3 = gt.GenericStepsWorker("FAKE03", {}, ctx={"job_id": "t3"}) def fake_exec_all(d, steps, depth=0): name = w3._vars.get('target_name') with app.app_context(): row = (models.VideoPlan.query.filter_by(nickname=name, status=vp.ST_READY) .order_by(models.VideoPlan.seq).first()) if row: row.status = vp.ST_DONE models.db.session.commit() w3._exec_steps = fake_exec_all w3._exec_foreach_account(None, {"children": [{"type": "x"}], "date_source": "fixed", "fixed_date": DATE}) check('小李补上了', status_of()['小李'], ['done']) print('=== 4. 没有待发的号 → 干净收工 ===') w4 = gt.GenericStepsWorker("FAKE04", {}, ctx={"job_id": "t4"}) w4._exec_steps = lambda d, s, depth=0: None w4._exec_foreach_account(None, {"children": [{"type": "x"}], "date_source": "fixed", "fixed_date": DATE}) check('一条都没跑(不会死循环)', w4._vars.get('target_name'), None) print('=== 5. max_accounts 上限 ===') with app.app_context(): models.VideoPlan.query.delete() models.db.session.commit() seed() w5 = gt.GenericStepsWorker("FAKE05", {}, ctx={"job_id": "t5"}) ran = [] w5._exec_steps = lambda d, s, depth=0: ran.append(w5._vars.get('target_name')) w5._exec_foreach_account(None, {"children": [{"type": "x"}], "max_accounts": 1, "date_source": "fixed", "fixed_date": DATE}) check('只跑 1 个号', ran, ['小李']) print('=== 6. 子步骤为空 → 不报错、直接结束 ===') w6 = gt.GenericStepsWorker("FAKE06", {}) w6._exec_foreach_account(None, {"children": []}) check('空 children 直接返回', w6._vars.get('target_name'), None) print() shutil.rmtree(TMP, ignore_errors=True) if fails: print('❌ 失败 %d 项:%s' % (len(fails), ' / '.join(fails))) sys.exit(1) print('✅ 全部通过(临时库已清理)')