
    KiM                    V   d Z ddlmZ ddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlmZ ddlmZ ddlmZ ddlZddlmZ dZd	Zd!d
Zd"d#dZ e        ddgZ ed      j9                         xs  ej:                  d      duZej>                  jA                  e d      Z!ejD                  d$d       Z#ejD                  d%d       Z$ ejD                  d      d&d       Z%d'dZ&d'dZ'd'dZ(e!d(d       Z)e!d)d       Z*e!d)d       Z+d*dZ,d+dZ-d*dZ.d*dZ/d*dZ0d'd Z1y),u=  IDS Phase 5 — 모션 카드뉴스 (HTML→MP4) 테스트.

테스트 커버리지:
1. SNS 규격 상수 검증 (3종)
2. 5가지 모션 효과 상수 검증
3. 알 수 없는 효과 ValueError 검증
4. 실제 ffmpeg로 MP4 렌더링 (L1 스모크)
5. 키프레임 추출 (첫/중/끝 3개)
6. OCR 검증 — pytesseract 없이 폴백
7. 큐 enqueue + process 성공
8. 큐 동시성 제한 검증
9. 큐 재시도 동작 검증
10. 큐 타임아웃 → TIMEOUT 상태
11. 외부 네트워크 호출 금지 검증 (§0.5)
12. BGM 라이선스 검증 — 거부 케이스
    )annotationsN)ThreadPoolExecutor)Path)patch)Imagez-/home/jay/workspace/skills/motion-cardnews-komotion_cardnews_koc                    t         } t        t              }dD ]  }|  d| }|t        j                  vst
        j                  j                  ||| dz        }||j                  J d| d|        t
        j                  j                  |      }| |_
        |t        j                  |<   	 |j                  j                  |        | t        j                  vrt
        j                  j                  | |dz  t        |      g      }||j                  J d|  d       t
        j                  j                  |      }| |_
        |t        j                  | <   	 |j                  j                  |       t        j                  |    S # t        $ r t        j                  |=  w xY w# t        $ r t        j                  | =  w xY w)	u8   스킬 패키지 전체를 로드하고 반환합니다.)sizeseffectsframesrenderocrbgm.z.pyfailed to load  from z__init__.py)submodule_search_locationsz from __init__.py)_SKILL_MODULE_NAMEr   
_SKILL_DIRsysmodules	importlibutilspec_from_file_locationloadermodule_from_spec__package__exec_module	Exceptionstr)pkg_name	skill_dirsubmodmod_namespecmod	init_specpkg_mods           H/home/jay/workspace/tests/design-team/test_ids_phase5_motion_cardnews.py_load_skill_packager*   '   s   !H Z IH Zq)3;;&>>99vhcN*D #(?l?SYRZZ`aj`kAll?..11$7C&CO$'CKK!''," s{{"NN::%(+I'7 ; 
	
 $)9)9)EtYaXbbsGttE..11)<& 'H	((1
 ;;x  )  KK)   	H%	s   0F!F. F+.G
c                   t        d      }| t        j                  v rt        j                  |    S t        j                  j                  | t        |            }||j                  J d|  d|        t        j                  j                  |      }|t        j                  | <   	 |j                  j                  |       |S # t        $ r t        j                  | =  w xY w)u8   렌더 큐 스크립트를 동적으로 로드합니다.z2/home/jay/workspace/scripts/motion_render_queue.pyr   r   )r   r   r   r   r   r   r    r   r   r   r   )module_alias
queue_pathr%   r&   s       r)   _load_queue_moduler.   Q   s    JKJs{{"{{<((>>11,JPD 7k?<.X^_i^j9kk7
..
)
)$
/C #CKK$ J  KK%s   (C C!/home/jay/.local/bin/ffmpegffmpegzffmpeg required but not found)reasonc                      y)u.   테스트용 소형 해상도 (속도 향상).   r4    r5       r)   
small_sizer7   r   s     r6   c                    t         j                  t         d   j                  }|\  }}g d}g d}g }t	        t        ||            D ].  \  }\  }	}
| d| dz  } ||||	|
|       |j                  |       0 |S )uN   3개의 단색 PIL 프레임을 생성하고 경로 목록을 반환합니다..frames))   2   r;   )r;      r;   )r;   r;   r:   )u	   첫번째u	   두번째u	   세번째frame_.png)r   r   r   generate_solid_frame	enumeratezipappend)tmp_pathr7   r?   whcolorstextsr   icolortextouts               r)   three_framesrL   x   s     ;;*<)=W'EF[[DAq:F3EF%c&%&89 =E46!D))Q5$4c Mr6   session)scopec                   t         st        j                  d       | j                  d      }t        j
                  t         d   j                  }t        j
                  t         d   j                  }d}g d}g d}g }t        t        ||            D ]4  \  }\  }	}
|d| d	z  } ||d
   |d   |	|
|       |j                  |       6 |dz  } ||||ddd       |S )u2   세션 공유 MP4 파일 (test 4용). 5초 분량.zffmpeg not availablesession_mp4r9   z.renderr3   ))r<   d   r;   )r;      r<   )rQ   r<   P   u   시작u   중간u   끝r=   r>   r      ztest_output.mp4fade
   gQ?)frame_pathsoutput_pathsizeeffectfpsduration_per_frame)_FFMPEG_AVAILABLEpytestskipmktempr   r   r   r?   render_motionr@   rA   rB   )tmp_path_factoryrC   r?   rb   rZ   rF   rG   rX   rH   rI   rJ   rK   outputs                r)   rP   rP      s     *+&&}5H;;*<)=W'EF[[KK#5"6g >?MMMD=F'E K%c&%&89  =E46!D))T!Wd1gudC@3 
 ))F Mr6   c                     t         j                  t         d   } | j                  }d|v sJ d       d|v sJ d       d|v sJ d       |d   dk(  sJ |d   d	k(  sJ |d   d
k(  sJ y)uT   instagram_reels(1080×1920), twitter(1920×1080), threads(1080×1080) 정의 확인.z.sizesinstagram_reelsu)   instagram_reels가 SIZES에 없습니다.twitteru!   twitter가 SIZES에 없습니다.threadsu!   threads가 SIZES에 없습니다.)8    )rj   ri   )ri   ri   N)r   r   r   SIZES)	sizes_modrk   s     r)   test_3_sns_sizes_definedrm      s    12&9:IOOE%R'RR%BBBBBB"#|333|+++|+++r6   c                 |    t         j                  t         d   } | j                  }dD ]  }||v rJ d| d        y)uO   fade, slide, zoom, dissolve, sequence 키가 EFFECTS에 존재하는지 확인..effects)rV   slidezoomdissolvesequenceu   효과 'u   '이 EFFECTS에 없습니다.N)r   r   r   EFFECTS)effects_modrt   r[   s      r)   test_5_motion_effects_definedrv      sR    ++!3 4H=>K!!GC S RHVH4Q"RR Sr6   c                     t         j                  t         d   } | j                  }t	        j
                  t        d      5   |ddd       ddd       y# 1 sw Y   yxY w)	uL   알 수 없는 효과 이름에 대해 ValueError가 발생하는지 확인.ro   u   알 수 없는 효과matchunknown_effect_xyz   g      ?)r\   durationN)r   r   r   get_effect_filterr_   raises
ValueError)ru   r}   s     r)   %test_get_effect_filter_unknown_raisesr      sZ    ++!3 4H=>K#55	z)@	A F.BEF F Fs   AA c                   | j                         s
J d|         | j                         j                  dk\  s#J d| j                         j                   d       t        t	        d      j
                  dz        }t	        |      j                         st        j                  d      xs d}|rt        j                  |dd	d
dddt        |       gdd      }|j                  dk(  rW|j                  j                         r<t        |j                  j                               }d|cxk  rdk  sn J d| d       yyyy)u_   실제 ffmpeg로 5초 MP4를 렌더링하고 파일 크기와 지속 시간을 검증합니다.u)   MP4 파일이 존재하지 않습니다: i   u#   MP4 파일이 너무 작습니다: z bytesr/   ffprobe z-verrorz-show_entrieszformat=durationz-ofz"default=noprint_wrappers=1:nokey=1T)capture_outputrJ   r   g      @g       @u:   MP4 지속 시간이 예상 범위를 벗어났습니다: u   초N)existsstatst_sizer    r   parentshutilwhich
subprocessrun
returncodestdoutstripfloat)rP   ffprobe_binresultr|   s       r)   !test_render_short_mp4_real_ffmpegr      sC    Z#L[M!ZZ%%-w1TU`UeUeUgUoUoTppv/ww- d89@@9LMK##%ll9-3T7!2;K 	  	
 !fmm&9&9&;V]]0023H()c)u-ghpgqqt+uu) '<! r6   c                B   t         j                  t         d   }|j                  }|dz  } || |      \  }}}d|fd|fd|ffD ]Y  \  }}	|	j	                         sJ | d|	        |	j                         j                  dkD  s
J | d       |	j                  d	k(  rYJ  y
)uE   MP4에서 첫/중간/끝 3개의 키프레임 PNG를 추출합니다..ocr	keyframesfirstmiddlelastu&    키프레임 파일이 없습니다: r   u#    키프레임이 비어있습니다r>   N)r   r   r   extract_keyframesr   r   r   suffix)
rP   rC   ocr_modr   
output_dirr   r   r   label
frame_paths
             r)   &test_extract_keyframes_returns_3_pathsr      s     kk/056G11K'J+KDE64&.60BVTNS +z  "`ug-ST^S_$``" ((1,[7Z.[[,  F***+r6   c                   t         j                  t         d   }|j                  }|dz  } || g d|      }t	        |t
              sJ d       t        |j                               h dk(  sJ dD ]c  }||   }t	        |t
              sJ t        |j                               h dk\  sJ t	        |d	   t              sJ t	        |d
   t              rcJ  y)uV   pytesseract 없이 validate_korean_frames가 3개 엔트리를 반환하는지 확인.r   ocr_outrT   )expected_korean_charsr   u   결과가 dict여야 합니다>   r   r   r   )r   r   r   >   framefallbackocr_texthas_expectedr   r   N)	r   r   r   validate_korean_frames
isinstancedictsetkeysbool)rP   rC   r   r   r   r   r   entrys           r)   (test_ocr_validate_korean_frames_fallbackr      s     kk/056G$;;I%J#9F fd#E%EE#v{{}!<<<<, 3u%&&&5::< $UUUU%/666%
+T2223r6   c           
     Z   t        d      }t        | dz        }|t        j                  d<   	 |j	                  |D cg c]  }t        |       c}t        | dz        ddgdddd	d
      }|j	                  |D cg c]  }t        |       c}t        | dz        ddgdddd	d
      }|j                  ddd      }|d   dk(  sJ |d   dk(  sJ |d   dk(  sJ 	 t        j                  j                  dd	       y	c c}w c c}w # t        j                  j                  dd	       w xY w)uS   2개 작업을 큐에 추가하고 처리 후 모두 SUCCESS인지 확인합니다.motion_render_queue_t7
queue_testMOTION_QUEUE_DIRzjob1_output.mp4r4   rV   rW         ?NrX   rY   rZ   r[   r\   r]   bgm_pathzjob2_output.mp4rs      <   rU   max_concurrenttimeout_per_jobmax_retriestotalsuccessfailedr   )r.   r    osenvironenqueueprocess_queuepop)rC   rL   mq	queue_dirpjob1_idjob2_idsummarys           r)   &test_queue_enqueue_and_process_successr     sE   	4	5BH|+,I%.BJJ!"1**,89qCF9x*;;<#J"%
  **,89qCF9x*;;<#J "%
  ""!RUV"Ww1$$$y!Q&&&x A%%%


)401 : : 	

)40s)   D C>,D ;DAD >
D "D*c           
        t        d      }t        | dz        }|t        j                  d<   	  G fdd      g _        dd}|j                  |D cg c]  }t        |       c}t        | dz        ddgd	d
ddd       t        j                  |d      5  t        j                  |d|      5  |j                  ddd       ddd       ddd       t        d j                  D              sJ dj                          	 t        j                  j                  dd       yc c}w # 1 sw Y   hxY w# 1 sw Y   lxY w# t        j                  j                  dd       w xY w)uM   ThreadPoolExecutor가 max_concurrent=3으로 호출되는지 확인합니다.motion_render_queue_t8queue_concurrencyr   c                  D    e Zd ZU g Zded<   dd	 fdZd
dZddZddZy)1test_queue_concurrency_limit.<locals>.SpyExecutorz	list[int]_callsc                ^    j                   j                  |       t        |      | _        y )N)max_workers)r   rB   r   _real)selfr   kwargsSpyExecutors      r)   __init__z:test_queue_concurrency_limit.<locals>.SpyExecutor.__init__:  s#    ""))+6/KH
r6   c                :    | j                   j                          | S N)r   	__enter__)r   s    r)   r   z;test_queue_concurrency_limit.<locals>.SpyExecutor.__enter__>  s    

$$&r6   c                6     | j                   j                  |  y r   )r   __exit__)r   argss     r)   r   z:test_queue_concurrency_limit.<locals>.SpyExecutor.__exit__B  s    #

##T*r6   c                B     | j                   j                  |g|i |S r   )r   submit)r   fnr   r   s       r)   r   z8test_queue_concurrency_limit.<locals>.SpyExecutor.submitE  s#    (tzz((=d=f==r6   N)rU   )r   intr   objectreturnNone)r   z'SpyExecutor')r   r   r   r   )r   r   r   r   r   r   r   r   )	__name__
__module____qualname__r   __annotations__r   r   r   r   )r   s   r)   r   r   7  s"     "FI"I+>r6   r   c                >    d| d<   t        j                          | d<   | S )NSUCCESSstatuscompleted_at)timejob_datas    r)   fast_executez2test_queue_concurrency_limit.<locals>.fast_executeK  s!    !*HX'+yy{H^$Or6   zconcurrency_dummy.mp4r4   rV   rW   r   Nr   r   _execute_jobside_effect   r{   r   r   c              3  &   K   | ]	  }|d k(    yw)r   Nr5   ).0rD   s     r)   	<genexpr>z/test_queue_concurrency_limit.<locals>.<genexpr>_  s     6a166s   u:   max_workers=3으로 호출되지 않았습니다. 실제: r   r   r   r   )r.   r    r   r   r   r   r   r   r   anyr   )rC   rL   monkeypatchr   r   r   r   r   s          @r)   test_queue_concurrency_limitr   /  sv   	4	5BH223I%.BJJ!"-1	> 	>"  	
 	

,89qCF9x*AAB#J"%
 	 \\"2K@ 	Vb.lK V  2ST UV	V
 6;#5#566  	J:t  vA  vH  vH  uI  9J  	J6 	

)40# :V V	V 	V 	

)40sM   &E D+'4E D<4D0	D<8E +E 0D9	5D<<EE "E*c           
     F   t        d      }t        | dz        }|t        j                  d<   	 |j	                  |D cg c]  }t        |       c}t        | dz        ddgdddd	d
       ddidfd}t        j                  |d|      5  |j                  ddd      }d	d	d	       j                  dd      dkD  s
J d|        	 t        j                  j                  dd	       y	c c}w # 1 sw Y   OxY w# t        j                  j                  dd	       w xY w)u]   render_motion이 실패하면 재시도하고 retry_total이 증가하는지 확인합니다.motion_render_queue_t9queue_retryr   zretry_output.mp4r4   rV   rW   r   Nr   nr   c                    dxx   dz  cc<   d   dk  rt        dd    d      d| d<   t        j                         | d<   | S )Nr  rU   u   의도적 실패 (시도 )r   r   r   )RuntimeErrorr   )r   attempt_counts    r)   patched_execute_jobz8test_queue_retry_on_failure.<locals>.patched_execute_jobx  s[    #!#S!Q&"%>}S?Q>RRS#TUU!*HX'+yy{H^$Or6   r   r   rU   r   r   r   retry_totalu+   재시도가 기록되지 않았습니다: r   
r.   r    r   r   r   r   r   r   getr   )rC   rL   r   r   r   r  r   r  s          @r)   test_queue_retry_on_failurer
  e  s#   	4	5BH},-I%.BJJ!"1


,89qCF9x*<<=#J"%
 	 a	 \\"n:MN 	\&&aYZ&[G	\ {{=!,q0i4_`g_h2ii0 	

)403 :&	\ 	\ 	

)40s4   C> C-?C> C2$'C> -C> 2C;7C> >"D c           
     ^   t        d      }t        | dz        }|t        j                  d<   	 |j	                  |D cg c]  }t        |       c}t        | dz        ddgdddd	d
       dd}t        j                  |d|      5  |j                  ddd      }d	d	d	       j                  dd      |j                  dd      z   dkD  s
J d|        	 t        j                  j                  dd	       y	c c}w # 1 sw Y   bxY w# t        j                  j                  dd	       w xY w)uW   render가 타임아웃을 초과하면 TIMEOUT 또는 FAILED 상태가 기록됩니다.motion_render_queue_t10queue_timeoutr   ztimeout_output.mp4r4   rV   rW   r   Nr   c                0    t        j                  d       | S )NrW   )r   sleepr   s    r)   slow_executez5test_queue_timeout_marks_failed.<locals>.slow_execute  s    JJrNOr6   r   r   rU   r   r   r   timeoutr   u5   타임아웃/실패가 기록되지 않았습니다: r   r  )rC   rL   r   r   r   r  r   s          r)   test_queue_timeout_marks_failedr    s*   	5	6BH./I%.BJJ!"1


,89qCF9x*>>?#J"%
 		 \\"n,G 	[&&aXY&ZG	[ Iq)GKK!,DDI 	NCG9M	NI 	

)40) :	[ 	[ 	

)40s4   D
 C99D
 C>:D
 9D
 >DD
 
"D,c           
        g dfd}t        d      }t        | dz        }|t        j                  d<   	 |j	                  |D cg c]  }t        |       c}t        | dz        ddgddd	d
d       t        d|      5  	 dd
l}t        j                  |d|      5  |j                  ddd      }d
d
d
       d
d
d
       rJ d       j                  dd      dkD  s
J d|        	 t        j                  j                  dd
       y
c c}w # 1 sw Y   `xY w# t        $ r |j                  ddd      }Y w xY w# 1 sw Y   xY w# t        j                  j                  dd
       w xY w)u;  렌더링 및 큐 처리 중 외부 HTTP API 호출이 없는지 확인합니다 (§0.5).

    urllib.request.urlopen 과 requests.get 을 모킹하여 외부 호출 여부를 감지합니다.
    socket.create_connection은 ffmpeg 서브프로세스와의 충돌을 피하기 위해 모킹하지 않습니다.
    c                 <    j                  d       t        d      )NTuP   외부 네트워크 호출이 금지되어 있습니다 (urllib.request.urlopen))rB   AssertionError)r   r   external_callss     r)   mock_urlopenz7test_no_external_api_direct_calls.<locals>.mock_urlopen  s    d#oppr6   motion_render_queue_t11queue_no_netr   zno_net_output.mp4r4   rV   rW   r   Nr   zurllib.request.urlopenr   r   r	  rU   r   r   u4   외부 네트워크 호출이 감지되었습니다!r   u;   렌더링 성공이 기대되었으나 실패했습니다: )r   r   r   r   r   r   )r.   r    r   r   r   r   requestsr   r   ImportErrorr	  r   )	rC   rL   r  r   r   r   r  r   r  s	           @r)   !test_no_external_api_direct_callsr    s    "$Nq 
5	6BH~-.I%.BJJ!"1


,89qCF9x*==>#J"%
 	 +F 	``\\(E|L d ..aQSab.cGd	` "Y#YY!{{9a(1,u0klskt.uu, 	

)40+ :d d `**!R]^*_`	` 	` 	

)40sk   E D*E ED  D5D =0E E D	D  E =E?E  EEE "E1c                    t         j                  t         d   } | j                  }| j                  }t        j                  t        d      5   |d       ddd       dddd	d
d|d<   	 t        j                  t        d      5   |d       ddd       dt        |j                               v r|d= yy# 1 sw Y   axY w# 1 sw Y   5xY w# dt        |j                               v r|d= w w xY w)ue   알 수 없는 트랙 ID와 허용되지 않은 라이선스에 대해 ValueError가 발생합니다.z.bgmu   알 수 없는 트랙rx   nonexistent_track_xyzNzBad License TrackunknownPROPRIETARYzhttps://placeholder.example/badzbad_license.mp3)namesourcelicenseurlfilenametest_bad_license_tracku    허용되지 않은 라이선스)
r   r   r   validate_licenseBGM_LIBRARYr_   r~   r   listr   )bgm_modr'  r(  s      r)   +test_bgm_license_validation_rejects_unknownr+    s    kk/056G//%%K 
z)@	A 2012
 $ 0%-K()6]]:-OP 	756	7 $tK,<,<,>'??45 @#2 2	7 	7 $tK,<,<,>'??45 @s0   	B7+C 	CC 7C CC !C0)r   r   )motion_render_queue)r,   r    r   r   )r   tuple[int, int])rC   r   r7   r-  r   
list[Path])rc   zpytest.TempPathFactoryr   r   )r   r   )rP   r   r   r   )rP   r   rC   r   r   r   )rC   r   rL   r.  r   r   )rC   r   rL   r.  r   zpytest.MonkeyPatchr   r   )2__doc__
__future__r   importlib.utilr   importlib.machineryr   r   r   r   r   urllib.requesturllibwarningsconcurrent.futuresr   pathlibr   unittest.mockr   r_   PILr   r   r   r*   r.   _ffmpeg_pathsr   r   r^   markskipif_FFMPEG_SKIPfixturer7   rL   rP   rm   rv   r   r   r   r   r   r   r
  r  r  r+  r5   r6   r)   <module>r?     ss    #   	   
    1    <
) '!T$   /9	&'..0 *v||HT)  {{!!&7"7@_!`  
   i  !D,SF v v4 + + 3 3. 1F31l!1H1>'1T6r6   